事件对象:事件信息的宝库
事件对象是什么
当你点击一个按钮、敲击键盘或移动鼠标时,浏览器不只是简单地触发一个事件那么简单。它会创建一个包含丰富信息的对象,记录事件发生的方方面面:在哪里发生、什么时候发生、涉及哪些键或按钮、发生在哪个元素上......这个对象就是事件对象(Event Object)。
把事件对象想象成一份详细的事件报告。就像警察到达案发现场时会记录时间、地点、涉及人员、物证等信息一样,事件对象记录了与该事件相关的所有数据。有了这份报告,我们的代码就能根据具体情况做出精确的反应。
每当事件处理器或监听器被调用时,浏览器会自动创建一个事件对象并作为第一个参数传入:
button.addEventListener("click", function (event) {
// event 就是事件对象
console.log(event);
});通用事件属性
所有类型的事件对象都继承自基础的 Event 接口,拥有一些通用属性。
type - 事件类型
type 属性是一个字符串,表示事件的类型:
button.addEventListener("click", function (event) {
console.log(event.type); // "click"
});
input.addEventListener("input", function (event) {
console.log(event.type); // "input"
});
document.addEventListener("DOMContentLoaded", function (event) {
console.log(event.type); // "DOMContentLoaded"
});如果同一个处理器用于处理多种事件,可以根据 type 来区分:
function handleEvent(event) {
switch (event.type) {
case "mouseenter":
element.style.backgroundColor = "lightblue";
break;
case "mouseleave":
element.style.backgroundColor = "";
break;
case "click":
console.log("点击了元素");
break;
}
}
element.addEventListener("mouseenter", handleEvent);
element.addEventListener("mouseleave", handleEvent);
element.addEventListener("click", handleEvent);target - 事件目标
target 属性指向实际触发事件的元素,也就是用户真正操作的那个元素:
<div id="container">
<button id="btn">
<span>点击我</span>
</button>
</div>const container = document.getElementById("container");
container.addEventListener("click", function (event) {
console.log("点击的元素:", event.target);
// 如果点击 span:<span>点击我</span>
// 如果点击 button:<button id="btn">...</button>
// 如果点击 div 的空白区域:<div id="container">...</div>
});target 在事件委托中特别有用,可以判断具体点击了哪个子元素:
const list = document.querySelector(".todo-list");
list.addEventListener("click", function (event) {
if (event.target.matches(".delete-btn")) {
deleteItem(event.target);
} else if (event.target.matches(".edit-btn")) {
editItem(event.target);
} else if (event.target.matches(".checkbox")) {
toggleItem(event.target);
}
});currentTarget - 当前处理元素
currentTarget 指向绑定事件处理器的元素,也就是调用 addEventListener 的那个元素:
<div id="outer">
<div id="inner">
<button id="btn">点击</button>
</div>
</div>const outer = document.getElementById("outer");
outer.addEventListener("click", function (event) {
console.log("target:", event.target.id); // btn(实际点击的)
console.log("currentTarget:", event.currentTarget.id); // outer(绑定事件的)
console.log("this:", this.id); // outer(this 指向 currentTarget)
});在普通函数中,this 关键字等同于 event.currentTarget:
button.addEventListener("click", function (event) {
console.log(this === event.currentTarget); // true
console.log(this === event.target); // true(如果直接点击 button)
});但在箭头函数中,this 继承外层作用域,与 currentTarget 无关:
button.addEventListener("click", (event) => {
console.log(this === event.currentTarget); // false
console.log(event.currentTarget); // button 元素
console.log(this); // 外层作用域的 this
});timeStamp - 事件时间戳
timeStamp 属性表示事件创建的时间,是一个从页面加载开始计算的毫秒数:
document.addEventListener("click", function (event) {
console.log("事件发生于:", event.timeStamp, "ms");
});可以利用时间戳来计算操作间隔:
let lastClickTime = 0;
button.addEventListener("click", function (event) {
const interval = event.timeStamp - lastClickTime;
if (interval < 300) {
console.log("双击检测");
}
lastClickTime = event.timeStamp;
});isTrusted - 是否可信
isTrusted 属性表示事件是由用户操作触发的(true),还是由脚本创建的(false):
button.addEventListener("click", function (event) {
if (event.isTrusted) {
console.log("真实的用户点击");
} else {
console.log("脚本触发的点击");
}
});
// 用户点击:isTrusted 为 true
// 代码触发:isTrusted 为 false
button.click();这个属性可以用来防止脚本自动触发的恶意操作。
事件流相关属性
eventPhase - 事件阶段
eventPhase 指示事件当前处于事件流的哪个阶段:
Event.NONE(0):事件未触发Event.CAPTURING_PHASE(1):捕获阶段Event.AT_TARGET(2):目标阶段Event.BUBBLING_PHASE(3):冒泡阶段
element.addEventListener(
"click",
function (event) {
console.log("当前阶段:", event.eventPhase);
switch (event.eventPhase) {
case Event.CAPTURING_PHASE:
console.log("捕获阶段");
break;
case Event.AT_TARGET:
console.log("目标阶段");
break;
case Event.BUBBLING_PHASE:
console.log("冒泡阶段");
break;
}
},
true
); // 捕获阶段bubbles - 是否冒泡
bubbles 属性表示该事件是否会冒泡:
button.addEventListener("click", function (event) {
console.log("是否冒泡:", event.bubbles); // true
});
button.addEventListener("focus", function (event) {
console.log("是否冒泡:", event.bubbles); // false
});大多数鼠标和键盘事件都会冒泡,但有些事件(如 focus、blur、scroll)不会冒泡。
cancelable - 是否可取消
cancelable 属性表示是否可以取消事件的默认行为:
link.addEventListener("click", function (event) {
console.log("可取消:", event.cancelable); // true
if (event.cancelable) {
event.preventDefault(); // 可以阻止跳转
}
});鼠标事件属性
鼠标事件对象(MouseEvent)包含与鼠标操作相关的详细信息。
鼠标位置
document.addEventListener("click", function (event) {
// 相对于视口(viewport)的坐标
console.log("clientX:", event.clientX);
console.log("clientY:", event.clientY);
// 相对于整个页面的坐标(包括滚动)
console.log("pageX:", event.pageX);
console.log("pageY:", event.pageY);
// 相对于屏幕的坐标
console.log("screenX:", event.screenX);
console.log("screenY:", event.screenY);
// 相对于目标元素的坐标
console.log("offsetX:", event.offsetX);
console.log("offsetY:", event.offsetY);
});坐标系统对比:
clientX/Y:相对于浏览器窗口可视区域,不包括滚动pageX/Y:相对于整个文档,包括滚动部分screenX/Y:相对于整个屏幕offsetX/Y:相对于触发事件的元素
实际应用中最常用的是 clientX/Y 和 pageX/Y:
// 创建跟随鼠标的提示框
document.addEventListener("mousemove", function (event) {
const tooltip = document.getElementById("tooltip");
tooltip.style.left = event.clientX + 10 + "px";
tooltip.style.top = event.clientY + 10 + "px";
});
// 在画布上绘制
canvas.addEventListener("click", function (event) {
const rect = canvas.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
drawPoint(x, y);
});鼠标按钮
element.addEventListener("mousedown", function (event) {
console.log("按钮编号:", event.button);
switch (event.button) {
case 0:
console.log("左键");
break;
case 1:
console.log("中键(滚轮)");
break;
case 2:
console.log("右键");
break;
}
});
element.addEventListener("click", function (event) {
// buttons 属性是位掩码,可以检测多个按钮同时按下
console.log("按钮状态:", event.buttons);
// 1 = 左键, 2 = 右键, 4 = 中键
});修饰键
检测是否同时按下了 Ctrl、Shift、Alt 或 Meta(Windows 键或 Command 键):
document.addEventListener("click", function (event) {
console.log("Ctrl:", event.ctrlKey);
console.log("Shift:", event.shiftKey);
console.log("Alt:", event.altKey);
console.log("Meta:", event.metaKey);
if (event.ctrlKey && event.key === "s") {
event.preventDefault();
console.log("Ctrl+S 快捷键");
}
});实际应用:
// 多选功能
itemList.addEventListener("click", function (event) {
if (!event.target.matches(".item")) return;
if (event.ctrlKey || event.metaKey) {
// Ctrl/Cmd + 点击:添加到选中项
toggleItemSelection(event.target);
} else if (event.shiftKey) {
// Shift + 点击:范围选择
selectRange(lastSelectedItem, event.target);
} else {
// 普通点击:单选
selectSingleItem(event.target);
}
});
// 右键菜单
element.addEventListener("contextmenu", function (event) {
if (event.shiftKey) {
// Shift + 右键:显示浏览器默认菜单
return;
}
event.preventDefault();
showCustomMenu(event.clientX, event.clientY);
});移动距离
movementX 和 movementY 表示鼠标相对于上次事件的移动距离:
let totalMovement = 0;
document.addEventListener("mousemove", function (event) {
const distance = Math.sqrt(event.movementX ** 2 + event.movementY ** 2);
totalMovement += distance;
console.log("总移动距离:", totalMovement);
});键盘事件属性
键盘事件对象(KeyboardEvent)包含按键信息。
key - 按键值
key 属性返回按下的键的字符串表示:
document.addEventListener("keydown", function (event) {
console.log("按键:", event.key);
// 字母和数字:'a', 'b', '1', '2'
// 特殊键:'Enter', 'Escape', 'Tab', 'Backspace'
// 方向键:'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'
// 功能键:'F1', 'F2', ... 'F12'
// 修饰键:'Control', 'Shift', 'Alt', 'Meta'
});实际应用:
// 快捷键
document.addEventListener("keydown", function (event) {
if (event.ctrlKey || event.metaKey) {
switch (event.key) {
case "s":
event.preventDefault();
saveDocument();
break;
case "z":
event.preventDefault();
undo();
break;
case "y":
event.preventDefault();
redo();
break;
}
}
if (event.key === "Escape") {
closeModal();
}
});
// 方向键导航
document.addEventListener("keydown", function (event) {
switch (event.key) {
case "ArrowUp":
event.preventDefault();
selectPreviousItem();
break;
case "ArrowDown":
event.preventDefault();
selectNextItem();
break;
case "Enter":
confirmSelection();
break;
}
});code - 物理键码
code 属性返回按键的物理位置代码,不受键盘布局影响:
document.addEventListener("keydown", function (event) {
console.log("key:", event.key); // 根据布局变化
console.log("code:", event.code); // 物理位置不变
// 美式键盘:key = 'q', code = 'KeyQ'
// 法式键盘:key = 'a', code = 'KeyQ'(同一个物理键)
});游戏开发中常用 code 来保证按键映射一致:
const keys = {};
document.addEventListener("keydown", function (event) {
keys[event.code] = true;
});
document.addEventListener("keyup", function (event) {
keys[event.code] = false;
});
function gameLoop() {
if (keys["KeyW"]) {
moveForward();
}
if (keys["KeyS"]) {
moveBackward();
}
if (keys["KeyA"]) {
moveLeft();
}
if (keys["KeyD"]) {
moveRight();
}
requestAnimationFrame(gameLoop);
}repeat - 是否重复
repeat 属性表示按键是否因长按而重复触发:
document.addEventListener("keydown", function (event) {
if (event.repeat) {
console.log("按键重复");
return; // 忽略重复事件
}
console.log("首次按下");
});控制事件行为的方法
事件对象提供了几个方法来控制事件的传播和默认行为。
preventDefault() - 阻止默认行为
很多事件都有浏览器预定义的默认行为,preventDefault() 可以阻止这些行为:
// 阻止表单提交
form.addEventListener("submit", function (event) {
event.preventDefault();
// 使用 AJAX 提交
const formData = new FormData(event.target);
submitFormViaAjax(formData);
});
// 阻止链接跳转
link.addEventListener("click", function (event) {
event.preventDefault();
// 使用路由导航
navigateTo(event.target.href);
});
// 阻止右键菜单
element.addEventListener("contextmenu", function (event) {
event.preventDefault();
// 显示自定义菜单
showCustomMenu(event.clientX, event.clientY);
});
// 阻止拖拽默认行为
dropZone.addEventListener("dragover", function (event) {
event.preventDefault(); // 允许放置
});
dropZone.addEventListener("drop", function (event) {
event.preventDefault(); // 阻止浏览器打开文件
const files = event.dataTransfer.files;
handleFiles(files);
});stopPropagation() - 停止传播
stopPropagation() 阻止事件继续传播(冒泡或捕获):
button.addEventListener("click", function (event) {
event.stopPropagation(); // 事件不会冒泡到父元素
console.log("只有这个处理器执行");
});
document.addEventListener("click", function () {
// 如果点击 button,这个处理器不会执行
console.log("点击了文档");
});典型应用场景:
// 模态框内部点击不关闭
const modal = document.querySelector(".modal");
const modalContent = modal.querySelector(".modal-content");
modal.addEventListener("click", function () {
closeModal(); // 点击背景关闭
});
modalContent.addEventListener("click", function (event) {
event.stopPropagation(); // 点击内容不关闭
});
// 下拉菜单
const dropdown = document.querySelector(".dropdown");
const menu = dropdown.querySelector(".menu");
dropdown.addEventListener("click", function (event) {
event.stopPropagation();
toggleMenu();
});
document.addEventListener("click", function () {
closeAllMenus(); // 点击其他地方关闭所有菜单
});stopImmediatePropagation() - 立即停止
stopImmediatePropagation() 不仅阻止事件传播,还会阻止同一元素上的其他监听器执行:
button.addEventListener("click", function (event) {
console.log("第一个监听器");
event.stopImmediatePropagation();
});
button.addEventListener("click", function () {
console.log("第二个监听器"); // 不会执行
});
button.addEventListener("click", function () {
console.log("第三个监听器"); // 不会执行
});
// 点击后只输出:第一个监听器而 stopPropagation() 只阻止传播,不影响同一元素上的其他监听器:
button.addEventListener("click", function (event) {
console.log("第一个监听器");
event.stopPropagation();
});
button.addEventListener("click", function () {
console.log("第二个监听器"); // 仍会执行
});
// 点击后输出:
// 第一个监听器
// 第二个监听器defaultPrevented - 检查是否已阻止
defaultPrevented 属性可以检查是否已经调用过 preventDefault():
link.addEventListener("click", function (event) {
if (!event.defaultPrevented) {
// 其他监听器没有阻止默认行为
console.log("将会跳转");
} else {
console.log("跳转已被阻止");
}
});实际应用场景
自定义上下文菜单
const contextMenu = document.getElementById("context-menu");
document.addEventListener("contextmenu", function (event) {
event.preventDefault();
// 定位菜单
contextMenu.style.left = event.clientX + "px";
contextMenu.style.top = event.clientY + "px";
contextMenu.style.display = "block";
// 存储目标元素
contextMenu.dataset.target = event.target.id;
});
// 点击其他地方关闭菜单
document.addEventListener("click", function () {
contextMenu.style.display = "none";
});
// 菜单项点击
contextMenu.addEventListener("click", function (event) {
event.stopPropagation();
const action = event.target.dataset.action;
const targetId = this.dataset.target;
const targetElement = document.getElementById(targetId);
switch (action) {
case "copy":
copyElement(targetElement);
break;
case "delete":
deleteElement(targetElement);
break;
case "edit":
editElement(targetElement);
break;
}
this.style.display = "none";
});拖拽排序
let draggedElement = null;
const items = document.querySelectorAll(".sortable-item");
items.forEach((item) => {
item.addEventListener("dragstart", function (event) {
draggedElement = this;
event.dataTransfer.effectAllowed = "move";
this.classList.add("dragging");
});
item.addEventListener("dragend", function () {
this.classList.remove("dragging");
});
item.addEventListener("dragover", function (event) {
event.preventDefault(); // 允许放置
event.dataTransfer.dropEffect = "move";
if (draggedElement !== this) {
// 根据鼠标位置判断插入位置
const rect = this.getBoundingClientRect();
const midpoint = rect.top + rect.height / 2;
if (event.clientY < midpoint) {
this.parentNode.insertBefore(draggedElement, this);
} else {
this.parentNode.insertBefore(draggedElement, this.nextSibling);
}
}
});
});键盘导航
class KeyboardNavigator {
constructor(items) {
this.items = Array.from(items);
this.currentIndex = 0;
this.init();
}
init() {
document.addEventListener("keydown", this.handleKeyDown.bind(this));
}
handleKeyDown(event) {
switch (event.key) {
case "ArrowDown":
case "j":
event.preventDefault();
this.selectNext();
break;
case "ArrowUp":
case "k":
event.preventDefault();
this.selectPrevious();
break;
case "Enter":
event.preventDefault();
this.activateCurrent();
break;
case "Home":
event.preventDefault();
this.selectFirst();
break;
case "End":
event.preventDefault();
this.selectLast();
break;
}
}
selectNext() {
this.currentIndex = Math.min(this.currentIndex + 1, this.items.length - 1);
this.updateSelection();
}
selectPrevious() {
this.currentIndex = Math.max(this.currentIndex - 1, 0);
this.updateSelection();
}
selectFirst() {
this.currentIndex = 0;
this.updateSelection();
}
selectLast() {
this.currentIndex = this.items.length - 1;
this.updateSelection();
}
updateSelection() {
this.items.forEach((item, index) => {
if (index === this.currentIndex) {
item.classList.add("selected");
item.scrollIntoView({ block: "nearest" });
} else {
item.classList.remove("selected");
}
});
}
activateCurrent() {
const current = this.items[this.currentIndex];
current.click();
}
}
// 使用
const navigator = new KeyboardNavigator(document.querySelectorAll(".nav-item"));双击编辑
const items = document.querySelectorAll(".editable-item");
items.forEach((item) => {
let clickCount = 0;
let clickTimer = null;
item.addEventListener("click", function (event) {
clickCount++;
if (clickCount === 1) {
clickTimer = setTimeout(() => {
// 单击
selectItem(this);
clickCount = 0;
}, 300);
} else if (clickCount === 2) {
// 双击
clearTimeout(clickTimer);
clickCount = 0;
startEditing(this);
}
});
});
function startEditing(element) {
const input = document.createElement("input");
input.value = element.textContent;
input.classList.add("edit-input");
const finishEdit = () => {
element.textContent = input.value;
input.remove();
};
input.addEventListener("blur", finishEdit);
input.addEventListener("keydown", (event) => {
if (event.key === "Enter") {
finishEdit();
} else if (event.key === "Escape") {
input.remove();
}
});
element.textContent = "";
element.appendChild(input);
input.focus();
input.select();
}总结
事件对象是事件系统中最核心的部分,它包含了事件的所有信息。通过本章学习,你应该掌握:
- 通用属性:
type、target、currentTarget、timeStamp等 - 鼠标事件:位置坐标、按钮状态、修饰键
- 键盘事件:
key、code、修饰键、repeat - 控制方法:
preventDefault()、stopPropagation()、stopImmediatePropagation() - 实际应用:自定义菜单、拖拽、键盘导航等