先记住这个答案
当事件在父子嵌套中冒泡时,target 始终指向原始触发元素,currentTarget 指向当前正在执行回调的挂载元素。在事件委托中,监听器绑定在父级,currentTarget 固定为父级,无法定位实际交互项,所以必须用 event.target 配合 closest 或 matches 来识别具体目标,才能实现正确分发。
- target 指向事件起点元素
- currentTarget 指向监听器挂载点
- 委托时用 target 定位具体点击对象
触发源头与处理节点的对照
当点击事件命中父容器中的子元素后,事件会冒泡回父层。父容器监听器运行时,event.target 永远指最初接收点击的那个子元素,即使冒泡经过多个层级也不改变;而 event.currentTarget 则是父容器本身,因为事件处理函数是挂在这上面的。只有当监听器绑在触发元素自身时,两者才会相等,其余阶段必然不同。
这种分化源于事件传播记录的是路径不同字段。target 是事件派发的起点,currentTarget 是当前派发到的节点。因此在一个嵌套节点各自绑定事件的页面里,每个回调中的 currentTarget 都是自己绑定的元素,而 target 永远相同。浏览器在回调执行完毕后会把 currentTarget 置为 null,所以它只存在于同步回调期间。
列表项动态点击的归属判断
假设一个待办列表 <ul id="tasks">,每个 <li> 有删除按钮。若直接在 li 上绑定 click,每次动态添加 li 都要重新 addEventListener。改为委托:只在 ul 上注册一次 click 监听,依靠 event.target 判断用户是按在哪个 li 或其上哪个删除按钮。这要求事件能冒泡到 ul,且过滤器能正确归一。
监听器内先用 let targetEl = event.target.closest('li') 把文本节点和子按钮都归一为最近的 li。如果 targetEl 是 null 或者它的父级不是当前列表,就 return。通过后执行删除或变更逻辑。这样后续新增的 li 也能触发同一监听,免去重复绑定,且无需维护监听器索引。
失效条件和处理代价
委托失效的典型条件是事件本身不冒泡,如 focus、mouseleave、scroll,它们不会到达父级,target 与 currentTarget 的差异无从发挥。这时需要改用捕获阶段或者 focusin/mouseleave 的替代事件。另一个易错点是异步读取:setTimeout 回调里访问 event.currentTarget 会拿到 null,因为浏览器已经重置属性。
要正确处理,应在同步回调中先把需要保留的元素引用存入局部变量,比如 const container = event.currentTarget,再在异步逻辑里使用该变量。对 target 也不必保留整个事件,最好把 event.target 也转成普通 DOM 引用。判断委托状态时,不要依赖 currentTarget 参与业务匹配,只认 target。
容易答错的地方
- 将 currentTarget 当作实际点击元素
- 在父级监听器里 currentTarget 固定是父元素,用 event.currentTarget.id 判断永远拿不到子项。正确是取 event.target 并用 closest 上溯。很多初级错误导致所有点击都认为发生在父容器。
- 在异步中把事件对象传出使用
- 事件回调结束令 currentTarget 为 null,若把 event 存入 setTimeout 再读 currentTarget 会得到 null,且 target 仍可读但已失去绑定上下文。应在回调最先同步拷贝所需字段或元素引用。
面试官还会怎么问?
点击 li 中的文字节点时,event.target 是文本还是节点?
浏览器会把文本节点的命中归一为其父元素,因此 event.target 通常是包含该文字的 li,而不是文本节点本身。手动分派事件时 target 可以是任意元素,但实际点击会遵循该规则。
在捕获阶段注册的监听器,target 和 currentTarget 何时不同?
捕获阶段发生在目标节点之前,所以祖先监听器最早执行,此时 currentTarget 是祖先,target 是目标;当派发到目标节点自身时二者暂时相等,随后冒泡阶段又分化。
委托时想排除点击特定类名怎么办?
可以先通过 event.target.closest('li') 找到主体,再检查该主体是否包含排除条件,如匹配 .disabled。在目标上调用 closest 不会跨出当前作用域。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。