Skip to content

事件对象:事件信息的宝库

事件对象是什么

当你点击一个按钮、敲击键盘或移动鼠标时,浏览器不只是简单地触发一个事件那么简单。它会创建一个包含丰富信息的对象,记录事件发生的方方面面:在哪里发生、什么时候发生、涉及哪些键或按钮、发生在哪个元素上......这个对象就是事件对象(Event Object)。

把事件对象想象成一份详细的事件报告。就像警察到达案发现场时会记录时间、地点、涉及人员、物证等信息一样,事件对象记录了与该事件相关的所有数据。有了这份报告,我们的代码就能根据具体情况做出精确的反应。

每当事件处理器或监听器被调用时,浏览器会自动创建一个事件对象并作为第一个参数传入:

javascript
button.addEventListener("click", function (event) {
  // event 就是事件对象
  console.log(event);
});

通用事件属性

所有类型的事件对象都继承自基础的 Event 接口,拥有一些通用属性。

type - 事件类型

type 属性是一个字符串,表示事件的类型:

javascript
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 来区分:

javascript
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 属性指向实际触发事件的元素,也就是用户真正操作的那个元素:

html
<div id="container">
  <button id="btn">
    <span>点击我</span>
  </button>
</div>
javascript
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 在事件委托中特别有用,可以判断具体点击了哪个子元素:

javascript
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 的那个元素:

html
<div id="outer">
  <div id="inner">
    <button id="btn">点击</button>
  </div>
</div>
javascript
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

javascript
button.addEventListener("click", function (event) {
  console.log(this === event.currentTarget); // true
  console.log(this === event.target); // true(如果直接点击 button)
});

但在箭头函数中,this 继承外层作用域,与 currentTarget 无关:

javascript
button.addEventListener("click", (event) => {
  console.log(this === event.currentTarget); // false
  console.log(event.currentTarget); // button 元素
  console.log(this); // 外层作用域的 this
});

timeStamp - 事件时间戳

timeStamp 属性表示事件创建的时间,是一个从页面加载开始计算的毫秒数:

javascript
document.addEventListener("click", function (event) {
  console.log("事件发生于:", event.timeStamp, "ms");
});

可以利用时间戳来计算操作间隔:

javascript
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):

javascript
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):冒泡阶段
javascript
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 属性表示该事件是否会冒泡:

javascript
button.addEventListener("click", function (event) {
  console.log("是否冒泡:", event.bubbles); // true
});

button.addEventListener("focus", function (event) {
  console.log("是否冒泡:", event.bubbles); // false
});

大多数鼠标和键盘事件都会冒泡,但有些事件(如 focusblurscroll)不会冒泡。

cancelable - 是否可取消

cancelable 属性表示是否可以取消事件的默认行为:

javascript
link.addEventListener("click", function (event) {
  console.log("可取消:", event.cancelable); // true

  if (event.cancelable) {
    event.preventDefault(); // 可以阻止跳转
  }
});

鼠标事件属性

鼠标事件对象(MouseEvent)包含与鼠标操作相关的详细信息。

鼠标位置

javascript
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/YpageX/Y

javascript
// 创建跟随鼠标的提示框
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);
});

鼠标按钮

javascript
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 键):

javascript
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 快捷键");
  }
});

实际应用:

javascript
// 多选功能
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);
});

移动距离

movementXmovementY 表示鼠标相对于上次事件的移动距离:

javascript
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 属性返回按下的键的字符串表示:

javascript
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'
});

实际应用:

javascript
// 快捷键
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 属性返回按键的物理位置代码,不受键盘布局影响:

javascript
document.addEventListener("keydown", function (event) {
  console.log("key:", event.key); // 根据布局变化
  console.log("code:", event.code); // 物理位置不变

  // 美式键盘:key = 'q', code = 'KeyQ'
  // 法式键盘:key = 'a', code = 'KeyQ'(同一个物理键)
});

游戏开发中常用 code 来保证按键映射一致:

javascript
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 属性表示按键是否因长按而重复触发:

javascript
document.addEventListener("keydown", function (event) {
  if (event.repeat) {
    console.log("按键重复");
    return; // 忽略重复事件
  }

  console.log("首次按下");
});

控制事件行为的方法

事件对象提供了几个方法来控制事件的传播和默认行为。

preventDefault() - 阻止默认行为

很多事件都有浏览器预定义的默认行为,preventDefault() 可以阻止这些行为:

javascript
// 阻止表单提交
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() 阻止事件继续传播(冒泡或捕获):

javascript
button.addEventListener("click", function (event) {
  event.stopPropagation(); // 事件不会冒泡到父元素
  console.log("只有这个处理器执行");
});

document.addEventListener("click", function () {
  // 如果点击 button,这个处理器不会执行
  console.log("点击了文档");
});

典型应用场景:

javascript
// 模态框内部点击不关闭
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() 不仅阻止事件传播,还会阻止同一元素上的其他监听器执行:

javascript
button.addEventListener("click", function (event) {
  console.log("第一个监听器");
  event.stopImmediatePropagation();
});

button.addEventListener("click", function () {
  console.log("第二个监听器"); // 不会执行
});

button.addEventListener("click", function () {
  console.log("第三个监听器"); // 不会执行
});

// 点击后只输出:第一个监听器

stopPropagation() 只阻止传播,不影响同一元素上的其他监听器:

javascript
button.addEventListener("click", function (event) {
  console.log("第一个监听器");
  event.stopPropagation();
});

button.addEventListener("click", function () {
  console.log("第二个监听器"); // 仍会执行
});

// 点击后输出:
// 第一个监听器
// 第二个监听器

defaultPrevented - 检查是否已阻止

defaultPrevented 属性可以检查是否已经调用过 preventDefault()

javascript
link.addEventListener("click", function (event) {
  if (!event.defaultPrevented) {
    // 其他监听器没有阻止默认行为
    console.log("将会跳转");
  } else {
    console.log("跳转已被阻止");
  }
});

实际应用场景

自定义上下文菜单

javascript
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";
});

拖拽排序

javascript
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);
      }
    }
  });
});

键盘导航

javascript
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"));

双击编辑

javascript
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();
}

总结

事件对象是事件系统中最核心的部分,它包含了事件的所有信息。通过本章学习,你应该掌握:

  1. 通用属性typetargetcurrentTargettimeStamp
  2. 鼠标事件:位置坐标、按钮状态、修饰键
  3. 键盘事件keycode、修饰键、repeat
  4. 控制方法preventDefault()stopPropagation()stopImmediatePropagation()
  5. 实际应用:自定义菜单、拖拽、键盘导航等