当一个事件被触发时,浏览器会生成一个事件对象(Event Object),并作为第一个参数传入事件处理函数。这个对象携带着关于此次事件的所有关键信息——谁触发的、在哪里触发的、什么类型的事件、按下的是哪个键……理解事件对象,才能精确控制交互行为。
15.3.1 获取事件对象
事件对象会自动传入,只需在处理函数中声明一个参数接收即可:
button.addEventListener('click', function(event) {
// event 就是事件对象
console.log(event);
});
// 箭头函数同样适用
button.addEventListener('click', (event) => {
console.log(event);
});
这个 event 参数是实打实的对象,不是可有可无的装饰。下面来看看它身上最常用的属性。
15.3.2 常用属性
event.type
事件类型,字符串形式。例如 'click'、'keydown'、'submit'。当一个处理函数被绑定到多种事件时,可以用 type 区分当前是哪种事件触发了回调。
element.addEventListener('mouseenter mouseleave', (e) => {
if (e.type === 'mouseenter') {
console.log('鼠标进入');
} else {
console.log('鼠标离开');
}
});
event.target
事件的真正触发者。即用户实际点击的那个 DOM 元素。这个属性在事件冒泡过程中始终指向最初触发事件的元素,不会改变。
event.currentTarget
当前正在处理该事件的元素。也就是绑定事件处理函数的那个元素。在事件冒泡过程中,currentTarget 会随着事件的向上传递而变化——它始终等于 this(在非箭头函数中)。
这两个属性在事件委托中尤为重要,我们来看一个具体例子:
<ul id="list">
<li>项目 1</li>
<li>项目 2</li>
<li>项目 3</li>
</ul>
const ul = document.getElementById('list');
ul.addEventListener('click', function(e) {
console.log(e.target); // 用户具体点击的那个 <li>
console.log(e.currentTarget); // 始终是 <ul>,因为事件绑定在这里
});
如果点击“项目 2”,输出:
e.target→<li>项目 2</li>e.currentTarget→<ul id="list">
这个区别是利用事件委托精确处理子元素事件的基础。
event.clientX / event.clientY
鼠标事件发生时,鼠标指针相对于浏览器视口(viewport)的水平和垂直坐标。如果你需要知道用户点击了页面的哪个位置,这是最直接的方式。
event.key 与 event.code
键盘事件专用。key 返回按键的显示值(如 'a'、'Enter'、'ArrowUp'),而 code 返回按键的物理位置代码(如 'KeyA'、'Enter'、'ArrowUp')。前者受键盘布局影响,后者不受,更适合游戏等需要固定按键位置的场景。
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
closeModal();
}
});
event.preventDefault() 是否存在
event.defaultPrevented 属性表示是否已经调用了 preventDefault(),在为复杂交互做条件判断时非常实用。
15.3.3 常用方法
event.preventDefault()
阻止浏览器的默认行为。什么是默认行为?点击一个链接会跳转页面,点击提交按钮会触发表单提交,按下空格键会向下滚动页面,这些就是浏览器为某些元素预置的默认行为。调用此方法后,这些行为不会发生。
// 阻止链接跳转
document.querySelector('a').addEventListener('click', (e) => {
e.preventDefault();
console.log('链接被点击,但不会跳转');
});
// 阻止表单提交
document.querySelector('form').addEventListener('submit', (e) => {
e.preventDefault();
// 用 JS 自行处理表单数据,比如通过 fetch 发送
});
注意:preventDefault() 不影响事件的传播(冒泡),它只是屏蔽了浏览器对当前事件的默认反应。
event.stopPropagation()
阻止事件继续冒泡或捕获。调用后,事件不会继续传递给父级或子级元素(取决于所处阶段),但在当前元素上如果有多个同类型事件处理函数,它们仍会全部执行。
document.querySelector('.child').addEventListener('click', (e) => {
e.stopPropagation();
console.log('子元素点击,事件不会冒泡到父元素');
});
document.querySelector('.parent').addEventListener('click', () => {
console.log('父元素被点击(冒泡上来的)');
});
// 点击子元素时,只会输出子元素的 log,父元素的事件不会触发
event.stopImmediatePropagation()
比 stopPropagation() 更强硬。它不仅阻止事件冒泡,还阻止当前元素上绑定的其他同类型事件处理函数执行。当一个元素绑定了多个 click 处理程序,而你又希望某个处理函数成为“终点”时,这个方法非常有用。
button.addEventListener('click', (e) => {
e.stopImmediatePropagation();
console.log('第一个处理函数,并阻止其他处理函数执行');
});
button.addEventListener('click', () => {
console.log('第二个处理函数,永远不会被调用');
});
通常情形下我们很少需要这么极端的控制,但了解它的存在可以在遇到怪异的执行顺序时多一个排查方向。
15.3.4 阻止默认行为 vs 阻止冒泡:一张对比表
| 方法 | 作用 | 影响范围 | 典型场景 |
|------|------|----------|----------|
| preventDefault() | 阻止浏览器预设行为 | 仅当前事件,不阻止传播 | 阻止链接跳转、表单提交、拖拽默认行为 |
| stopPropagation() | 阻止事件冒泡/捕获 | 阻止事件传递给祖先/后代 | 避免子元素点击触发父容器事件 |
| stopImmediatePropagation() | 阻止冒泡 + 阻止同元素其他处理函数 | 阻止后续所有同类事件处理 | 按钮只允许某一个处理函数生效 |
两个方法可以结合使用,互不冲突:
link.addEventListener('click', (e) => {
e.preventDefault(); // 不跳转
e.stopPropagation(); // 也不冒泡给父级菜单
// 执行自定义逻辑...
});
15.3.5 实战提醒
- 不要滥用
stopPropagation():它切断了事件流,可能会让上层的事件委托失效,导致全局性的行为(如关闭弹窗)无法正常工作。大多数情况下,通过e.target判断点击来源即可精确处理,无需完全阻断冒泡。
preventDefault()并非总是有效:某些事件是不可取消的(cancelable: false),比如scroll事件的默认行为就无法用此方法阻止。可以在事件对象上查看cancelable属性:e.cancelable === true时才能调用preventDefault()。
- 在事件委托中依靠
target而非阻断:委托场景下,父元素需要捕获子元素冒上来的事件,此时调用stopPropagation()等于自毁长城。正确的做法是:
parent.addEventListener('click', (e) => {
if (e.target.matches('.btn')) {
// 处理子按钮的点击
}
});
这样既不破坏事件流,又能精确响应所需元素。
掌握事件对象的核心属性和方法,并结合事件流的三个阶段,你就能在任何复杂的嵌套交互中做到精确控制,既不误触发,也不漏触发。这是实现可靠交互体验的基石,也是下一节深入事件委托与自定义事件的理论前提。