- 文档
- 教程
- 前端
【免费下载链接】zh.javascript.info
现代 JavaScript 教程(The Modern JavaScript Tutorial),以最新的 ECMAScript 规范为基准,通过简单但足够详细的内容,为你讲解从基础到高阶的 JavaScript 相关知识。
本文基于《现代 JavaScript 教程》(zh.javascript.info)「浏览器事件简介」章节中的编程任务「哪个处理程序会运行?」展开。文章完整还原题目与官方解答,并结合章节正文与源码,深入讲解
removeEventListener为什么要求「完全相同的函数对象」、匿名函数为何无法被移除,以及addEventListener与button.onclick这两套处理程序体系是如何互不干扰地共存的。读完本文,你将掌握事件处理程序的可移除性规则,并能写出正确的"添加—移除"配对代码。
一、题目回顾:三种处理程序操作混在同一段代码里
本任务位于仓库 2-ui/2-events/01-introduction-browser-events/03-which-handlers-run/ 目录下,对应的题目文件内容如下:
在变量中有一个按钮。它上面没有处理程序。
执行以下代码之后,哪些处理程序会在按钮被点击时运行?会显示哪些 alert?
button.addEventListener("click", () => alert("1")); button.removeEventListener("click", () => alert("1")); button.onclick = () => alert(2);这道题的「陷阱」在于:三行代码分别使用了addEventListener、removeEventListener和 DOM 属性onclick三种不同的处理程序操作方式,而它们各自的语义并不相同。官方解答给出的答案是:
答案:
1和2。
也就是说,点击按钮时两个 alert 都会弹出:先是1,然后是2。下面我们从三个层面拆解为什么。
二、第一层:为什么匿名函数的 removeEventListener 是"无效操作"
题目第二行代码:
button.removeEventListener("click", () => alert("1"));表面上它试图移除第一行addEventListener注册的处理程序,但实际上移除操作完全没有生效。原因在于:removeEventListener要求传入的必须是「被添加时那个完全相同的函数对象」,而这里传入的是一个新的匿名箭头函数——虽然它的代码和第一个箭头函数一模一样,但它们是两个不同的函数对象。
官方解答对此的解释是:
要移除处理程序,我们需要传递正确的所分配的函数。在代码中,传递了一个新的函数,该函数看起来相同,但仍然是另一个函数。
这一规则在章节正文 article.md 中有更详细的警告框说明:
要移除处理程序,我们需要传入与分配的函数完全相同的函数。 这不起作用: ```js no-beautify elem.addEventListener( "click" , () => alert('Thanks!')); // .... elem.removeEventListener( "click", () => alert('Thanks!')); ``` 处理程序不会被移除,因为 `removeEventListener` 获取了另一个函数 —— 使用相同的代码,但这并不起作用,因为它是一个不同的函数对象。用语言描述就是:addEventListener和removeEventListener在浏览器内部都是**按函数对象的身份(引用)**进行匹配的,而不是按代码文本匹配。两个各自创建的箭头函数字面量,即使源码完全相同,也是堆上两个不同的对象,身份不同,自然无法命中。
三、第二层:如何正确移除——存储函数引用
要移除一个由addEventListener添加的处理程序,唯一可靠的办法是:在添加时把函数存进变量(或用具名函数声明),移除时传入同一个引用。官方解答给出了标准写法:
function handler() { alert(1); } button.addEventListener("click", handler); button.removeEventListener("click", handler);这里handler是同一个函数对象:addEventListener注册的是它,removeEventListener移除的也是它,因此移除成功,点击按钮时不会再触发任何由addEventListener注册的click处理程序。
章节正文还强调了一个容易被忽略的事实:
请注意 —— 如果我们不将函数存储在一个变量中,那么我们就无法移除它。由
addEventListener分配的处理程序将无法被"读回"。
也就是说,处理程序一旦以匿名函数的形式传给addEventListener,就再没有任何 API 能把它"查出来"再移除——浏览器不会提供类似getEventListeners的读取接口(在常规 Web 环境下),你唯一能做的就是在添加时就保存好引用。这是编写可清理代码时必须遵守的纪律:凡是可能被移除的处理程序,一律用具名函数或变量引用。
3.1 常见的错误示范对比
| 写法 | 能否成功移除 | 原因 |
|---|---|---|
addEventListener("click", () => alert(1))后removeEventListener("click", () => alert(1)) | 不能 | 两个箭头函数是不同的函数对象 |
addEventListener("click", handler)后removeEventListener("click", handler) | 能 | 同一个函数对象被两次传入 |
addEventListener("click", handler)后removeEventListener("click", handler.bind(...))等变体 | 不能 | bind会生成新函数对象,同样不匹配 |
四、第三层:addEventListener 与 onclick 互不覆盖
题目第三行代码:
button.onclick = () => alert(2);onclick是 DOM 属性(property),它是独立于addEventListener体系之外的另一套处理程序分配机制。因此,无论addEventListener是否成功注册了处理程序、无论它注册了几个,button.onclick的赋值都会独立生效。官方解答最后一句点明了这一点:
无论
addEventListener怎样,button.onclick处理程序都会触发。
这也是为什么最终答案是1和2两个 alert 都会弹出:
alert("1")—— 由addEventListener注册(removeEventListener 未命中,故保留);alert("2")—— 由button.onclick赋值(与前者互不影响)。
4.1 两种体系的根本差异
章节正文对这两套机制的区别讲得很清楚:
- DOM 属性(property)方式:
elem.onclick = function。一个事件只能有一个处理程序,新赋值会覆盖旧赋值;要移除就用elem.onclick = null。它是大小写敏感的,必须写成elem.onclick而非elem.ONCLICK。 - 方法(method)方式:
elem.addEventListener(event, handler[, options])/elem.removeEventListener(event, handler[, options])。同一个事件可以注册多个处理程序,全部按添加顺序触发,互不覆盖。
正文还演示了两者可以同时使用:
<input id="elem" type="button" value="Click me"/> <script> function handler1() { alert('Thanks!'); }; function handler2() { alert('Thanks again!'); } elem.onclick = () => alert("Hello"); elem.addEventListener("click", handler1); // Thanks! elem.addEventListener("click", handler2); // Thanks again! </script>点击后三个 alert 都会依次弹出:先是Hello(来自onclick属性),然后是Thanks!、Thanks again!(来自两个addEventListener注册的处理程序)。这正是本题场景的放大版:addEventListener的处理程序和onclick属性处理程序是两条独立轨道,互不覆盖、都会执行。
不过正文也给出了务实建议:通常我们只使用其中一种方式,混用反而会让代码行为难以预测。
五、addEventListener 的 options 参数与相关细节
既然本题的核心是addEventListener,这里顺带补齐它的完整签名与可选参数(详见 article.md):
element.addEventListener(event, handler[, options]);event:事件名,例如"click"。handler:处理程序(函数或带handleEvent方法的对象)。options:附加可选对象,常用属性包括:once:为true时,处理程序在被触发一次后会自动删除(自动移除);capture:事件处理阶段(捕获/冒泡),由于历史原因也可以是布尔值false/true,等价于{capture: false/true};passive:为true时处理程序不会调用preventDefault()。
对应地,移除语法为:
element.removeEventListener(event, handler[, options]);注意removeEventListener的options也要与添加时保持一致(尤其涉及capture阶段时),否则同样可能匹配失败。
另外,addEventListener还支持对象处理程序:传入一个带有handleEvent(event)方法的对象,事件发生时浏览器会调用obj.handleEvent(event),也可以用同一个对象监听多个事件类型。正文以Menu类为例展示了这种用法——处理程序不再是无名函数,代码更易组织与维护。
六、拓展:哪些事件只能用 addEventListener
与本题相关的另一个知识点是:并非所有事件都能通过 DOM 属性赋值。例如DOMContentLoaded事件就没有对应的onDOMContentLoaded属性可用:
// 永远不会运行 document.onDOMContentLoaded = function() { alert("DOM built"); }; // 这种方式可以运行 document.addEventListener("DOMContentLoaded", function() { alert("DOM built"); });这解释了为什么正文总结中把addEventListener称为"最灵活"的方式——它是唯一能覆盖全部事件的通用注册接口。
七、总结:本题背后的三条核心规则
回到题目本身,把三段代码拆开看:
addEventListener("click", () => alert("1"))成功注册了一个处理程序;removeEventListener("click", () => alert("1"))因传入的是全新函数对象而匹配失败,处理程序仍然保留;button.onclick = () => alert(2)走 DOM 属性轨道,独立生效,不会覆盖也无需关心addEventListener的状态。
因此点击按钮时依次弹出1和2。
本题浓缩了三条值得牢记的事件处理规则,它们全部可以在仓库正文 2-ui/2-events/01-introduction-browser-events/article.md 与官方解答中找到依据:
- 移除必须传同一个函数对象:
removeEventListener按函数身份匹配,匿名函数"看上去一样"也救不了它; - addEventListener 与 onclick 是两条独立轨道:DOM 属性(property)同一事件只能有一个处理程序、可被覆盖;方法体系支持多个处理程序共存,两者互不覆盖;
- 可移除性是代码设计问题:需要动态增删处理程序时,务必保存函数引用或使用具名函数,否则注册后便无法移除,可能造成重复触发或内存占用问题。
如果你希望继续深入这个章节的完整知识体系(包括this指向、事件对象、handleEvent等),可以直接阅读仓库中的浏览器事件简介;想验证本节所学,可以继续完成同目录下的其他任务,例如点击隐藏按钮与小球移动。
- 文档
- 教程
- 前端
【免费下载链接】zh.javascript.info
现代 JavaScript 教程(The Modern JavaScript Tutorial),以最新的 ECMAScript 规范为基准,通过简单但足够详细的内容,为你讲解从基础到高阶的 JavaScript 相关知识。
相关推荐
现代 JavaScript 教程:对象方法、this 与箭头函数的运行时绑定机制
现代 JavaScript 教程:对象方法、this 与箭头函数的运行时绑定机制 对象是 JavaScript 中组织数据的基本单位,而 方法 (method)
文档教程前端Claudian 插件实战:5 步装好 Obsidian AI 助手,让 AI 帮你整理知识库
Claudian 插件实战:5 步装好 Obsidian AI 助手,让 AI 帮你整理知识库 整理一堆零散笔记时,最耗时间的就是把它们重新归位。装上 Clau
AI 应用代码智能体交互助手人工智能AI Agent现代 JavaScript 教程:深入理解 JavaScript 对象(Object)核心机制
现代 JavaScript 教程:深入理解 JavaScript 对象(Object)核心机制 对象(Object)是 JavaScript 中最重要的数据类型
文档/教程前端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考