先记住这个答案
用普通函数给元素注册监听器,事件触发时浏览器调用这个函数,会把 this 设成挂监听器的那个元素,也就是 event.currentTarget。如果回调是箭头函数,箭头函数没有自己的 this,拿到的是定义它的地方外层的 this,在模块顶层是 undefined,在 class 方法里是实例,反正不会是元素。实际项目里更推荐直接读 event.currentTarget 和 event.target,意思更清楚,也不怕以后有人把回调改成箭头函数。
- 普通函数回调里
this等于event.currentTarget - 箭头函数回调的
this来自外层,不是元素 target是点中的元素,currentTarget是挂监听器的元素- 异步回调里
currentTarget会变成null,要先存下来
事件委托里对比 this、target 和 currentTarget
把监听器挂在列表 ul 上,点里面某个按钮里的图标。这时三个值各不相同,最能看出区别。
const list = document.querySelector('#list')
list.addEventListener('click', function (event) {
console.log(this === list) // true:普通函数,this 是挂监听器的 ul
console.log(this === event.currentTarget) // true
console.log(event.target) // 点到的 <i> 图标,不是按钮
const button = event.target.closest('button[data-id]')
if (!button || !list.contains(button)) return
console.log(button.dataset.id) // 真正要处理的按钮
})
list.addEventListener('click', (event) => {
console.log(this === list) // false:箭头函数的 this 来自外层
})事件从 <i> 开始冒泡,经过 button,到达 ul 时执行监听器。target 一直是最初点中的 <i>,currentTarget 和 this 是当前这一层 ul。做事件委托时要用 target.closest() 往上找到真正关心的按钮,再用 contains 确认它确实在这个列表里,防止找到列表外面的祖先元素。
为什么不同写法的 this 不一样
this 由函数怎么被调用决定。浏览器派发事件时,相当于用 listener.call(currentTarget, event) 调用你的普通函数,所以 this 就是当前元素。箭头函数不接受调用方传进来的 this,call 也改不了它。
- 其它几种注册方式
- 写在 HTML 里的
onclick="console.log(this)",以及el.onclick = function () {},this同样是这个元素。还有一种不太常见的写法:传一个带handleEvent方法的对象给addEventListener,浏览器会调用obj.handleEvent(event),这时this是这个对象,不是元素。这种写法适合把状态和处理逻辑放在一个对象里,解绑时也传同一个对象。 - class 组件里想同时拿到实例和元素
- 在 class 里用箭头函数字段做回调,
this是实例,方便读写组件状态;需要元素就读event.currentTarget。两件事分开拿,不要为了用this拿元素而放弃箭头函数,也不要为了拿实例去bind之后又忘了元素从哪来。
容易答错的地方
- 把
target当成挂监听器的元素 target是事件最初发生的地方,可能是按钮里的文字、图标或者任何子元素。要拿挂监听器的元素,用currentTarget。按钮里放了图标之后点击逻辑失灵,基本都是这个原因。- 在异步代码里读
currentTarget - 事件派发结束后浏览器会把
currentTarget清空。回调里先await一个请求,再读event.currentTarget,得到的是null。需要的话在回调开头先存到变量里。 - 以为箭头函数回调里
this也是元素 - 箭头函数的
this在定义时就确定了,浏览器传进来的this被忽略。用了箭头函数又想拿元素,只能读event.currentTarget。
面试官还会怎么问?
target 和 currentTarget 什么时候相等?
事件就发生在挂监听器的元素本身时,比如直接点在按钮上、而不是按钮里的子元素上,两者相等。冒泡或捕获经过的祖先元素上,两者就不同了。
removeEventListener 为什么有时候移不掉?
它按事件类型、回调函数引用和 capture 标志来匹配,三样都要一致。this 不参与匹配,但如果注册时传的是 handler.bind(this),解绑时再 bind 一次就是另一个函数,自然移不掉。要么把绑定后的函数存起来,要么用 AbortController 的 signal 统一移除。
Shadow DOM 里的事件,外面读到的 target 对吗?
事件冒泡出 Shadow DOM 边界时,target 会被重定向成宿主元素,外面看不到内部的具体节点。需要完整路径时用 event.composedPath(),前提是这个 shadow root 是 open 模式。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。