# JavaScript教程 - 16 DOM

DOM 继续!

# 16.12 事件

重新来说回事件。

# 1 事件对象

什么是事件对象?

浏览器在事件触发时自动创建的一个特殊对象,包含了与该事件相关的所有信息和功能(包括鼠标、键盘等信息)。例如鼠标相关的事件,会包含事件触发时候的鼠标位置。

举个栗子:

鼠标在 div 中移动,获取事件对象。

<body>
  <div id="div1" style="width: 100px; height: 100px; background-color: lightblue;"></div>

  <script>
    const div1 = document.getElementById('div1');

    div1.onmousemove = function (event) {
      div1.innerHTML = event.x + ", " + event.y;  // 可以获取事件的详细信息
    };
  </script>
</body>
1
2
3
4
5
6
7
8
9
10
11
  • 在上面的代码中,给 div 添加鼠标移动事件回调方法,回调方法的第一个参数就是事件对象。
  • 我们可以从事件对象中获取到鼠标当前的位置,event.xevent.y 表示鼠标相对于浏览器窗口的坐标,event.xevent.y 也可以使用 clientXclientY

运行如下:

同样,通过箭头函数或者 addEventListener 事件监听的方式也可以获取。

div1.onmousemove = event => {
  div1.innerHTML = event.x + ", " + event.y;
}

div1.addEventListener("mousemove", function () {
  div1.innerHTML = event.x + ", " + event.y;
});

div1.addEventListener("mousemove", event => {
  div1.innerHTML = event.x + ", " + event.y;
});
1
2
3
4
5
6
7
8
9
10
11

# 2 事件的常用属性

不同的事件,事件对象是不同的,但是它们都继承自 Event 对象。

<body>
  <div id="div1" style="width: 100px; height: 100px; background-color: lightblue; margin: 100px;"></div>

  <script>
    const div1 = document.getElementById('div1');

    div1.onclick = function (event) {
      console.log(event.offsetX + ', ' + event.offsetY);  // 鼠标相对于事件目标元素的坐标
      console.log(event.x + ', ' + event.y);  // 鼠标相对于浏览器窗口的坐标
      console.log(event.screenX + ', ' + event.screenY);  // 鼠标相对于屏幕的坐标
      console.log(event.pageX + ', ' + event.pageY);  // 鼠标相对于整个文档的坐标(包含滚动)
    };
  </script>
</body>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
  • 上面通过一些方式获取到了鼠标相对于不同对象的位置。

# 3 事件冒泡

什么是事件冒泡?

事件冒泡是指当 DOM 元素上的事件被触发时,这个事件会从该元素向上传播到它的父元素、祖先元素,直到 document 根节点。

例如,div1 > div2 > div3 三个 div 嵌套,当点击 div3 的时候,会触发 div3 的点击事件,然后点击事件会向上传播给 div2,然后继续传播给 div1

演示一下:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>For技术栈</title>

  <style>

    #div1 {
      width: 200px;
      height: 200px;
      background-color: lightblue;
    }

    #div2 {
      width: 150px;
      height: 150px;
      background-color: lightgreen;
    }

    #div3 {
      width: 100px;
      height: 100px;
      background-color: pink;
    }
    
  </style>
</head>

<body>
  <div id="div1">
    <div id="div2">
      <div id="div3"></div>
    </div>
  </div>

  <script>
    const div1 = document.getElementById('div1');
    const div2 = document.getElementById('div2');
    const div3 = document.getElementById('div3');

    div1.addEventListener('click', ) = function (event) {
      alert('点击div1');
    };
    div2.onclick = function (event) {
      alert('点击div2');
    };
    div3.onclick = function (event) {
      alert('点击div3');
    };

  </script>
</body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56

显示如下:

当点击 div3 的时候,会依次弹出 点击div3点击div2点击div1 ,一次传递给父元素。

需要注意,事件的冒泡,和事件的订阅是没有关系的,div3没有订阅点击事件,事件也是会传递给 div2 和 div1 的。另外和 CSS 样式无关,div3 在 div2 中,即使通过 CSS 样式,让 div3 显示到 div2 的外面,div3 的样式还是会传递给 div2 和 div1,因为从 HTML 结构上它们是父子关系。

事件的冒泡对于我们的开发是有利的,会简化我们的开发。

当然,我们也可以在 div3 的点击事件中,取消点击事件的冒泡,这样 div2 和 div1 就收不到事件了。

举个例子:

div3.onclick = function (event) {
  alert('点击div3');
  event.stopPropagation();  // 停止事件冒泡
};
1
2
3
4
  • 在 div3 的点击事件处理函数中,可以通过 event.stopPropagation(); 停止事件的冒泡。

# 4 触发事件的对象

在事件的回调函数中,我们可以获取到触发事件的对象,还可以获取绑定事件的对象。

举个栗子:

如果事件是从 div3 传递给 div1 的,那么在 div1 的回调函数中

div1.onclick = function (event) {
  console.log(event.target);  // 触发事件的对象<div id="div3">
  console.log(this);  // 绑定事件的对象,<div id="div1">
  console.log(event.currentTarget); // 绑定事件的对象,同this <div id="div1">
};
1
2
3
4
5
  • event.target 表示当前触发事件的对象,事件是 div3 触发的传递给 div1 的,那么这里 target 还是 div3。
  • this(非箭头函数)表示绑定事件的对象,这里是 div1,event.currentTarget 表示绑定事件的对象,所以和 this 相同,如果在 箭头函数中,没有办法获取到 this,那么可以使用 event.currentTarget 获取到绑定事件的对象。
  • 如果在 div3 的事件回调函数中,那么 event.targetthisevent.currentTarget 是相同的。

# 5 阻止默认行为

<a> 标签在点击的时候,会跳转到 href 属性指定的地址,在讲解 HTML5 的时候说过,可以通过如下方式阻止 <a> 标签的默认行为:

<a href="javascript:void(0);">点击我什么都不会发生</a>
<!-- 或者 -->
<a href="javascript:;">点击我什么都不会发生</a>
1
2
3

有时候我们需要通过监听 <a> 的点击事件,做一些处理,那么可以像上面这样禁用 <a> 标签的默认行为。

除了上面这种方式,我们还可以在 <a> 标签的点击事件中,通过 event.preventDefault(); 阻止 <a> 标签的默认行为:

<a id="my-a" href="https://www.foooor.com">For技术栈</a>

<script>
const myA = document.getElementById('my-a');
myA.onclick = function(event) {
  event.preventDefault();  // 阻止默认行为

  alert('点击了a标签');
};
</script>
1
2
3
4
5
6
7
8
9
10

# 6 事件的委派

什么是事件的委派?

事件委派(Event Delegation)是 JavaScript 中的一种事件处理技巧,它利用事件冒泡,把多个子元素的事件,统一交给父元素来监听和处理,从而减少事件绑定数量、提高性能和灵活性。

举个栗子:

查看一下下面的两种事件绑定方式。

<body>
  <ul id="list">
    <li>苹果</li>
    <li>香蕉</li>
    <li>橘子</li>
  </ul>

  <script>
    // 方式一:传统方式
    const liItems = document.querySelectorAll("#list li");
    // 遍历liItems,给每个li添加点击事件
    liItems.forEach((li) => {
      li.addEventListener("click", () => {
        console.log("你点击了", li.textContent);
      });
    });

    // 方式二:事件委派方式
    const listEle = document.getElementById("list");
    // 给ul添加点击事件
    listEle.addEventListener("click", (e) => {
      // 将liItems转换为数组,判断点击的是否是数组中的li
      const liArray = Array.from(liItems);
      if (liArray.includes(e.target)) {
        console.log("你点击了", e.target.textContent);
      }
    });
  </script>
</body>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
  • 在上面的代码中,方式一是给所有的元素都绑定事件;
  • 方式二是将元素的绑定事件委托给父元素,当点击子元素的时候,会通过事件冒泡传递给父元素,然后在判断点击的是哪个子元素,这样就减少了事件的绑定数量。

如果我们此时向父元素中动态的添加子元素,通过第二种事件委派的方式,就不需要动态添加的元素添加绑定事件,从而简化了处理。

# 7 事件的传播机制

事件的传播其实是经历三个阶段的:

  1. 捕获阶段:事件从最外层(window / document)开始,也就是事件一开始是由祖先元素捕获,然后从祖先元素依次向子元素传播,直到传播到目标元素。

  2. 目标阶段:事件到达真正的目标元素,此阶段事件会在目标元素本身触发。

  3. 冒泡阶段:事件从目标元素开始,逐层向上传播回祖先元素,直到 document

也就是事件是由祖先元素捕获,然后传播给目标元素,然后再由目标元素逐层向上传播回祖先元素。

我们在监听事件进行处理的时候,默认是在冒泡阶段触发的,可以通过监听事件的第三个参数,将触发阶段修改到捕获阶段。

举个栗子:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>For技术栈</title>

  <style>

    #div1 {
      width: 200px;
      height: 200px;
      background-color: lightblue;
    }

    #div2 {
      width: 150px;
      height: 150px;
      background-color: lightgreen;
    }

    #div3 {
      width: 100px;
      height: 100px;
      background-color: pink;
    }
    
  </style>
</head>

<body>
  <div id="div1">
    <div id="div2">
      <div id="div3"></div>
    </div>
  </div>

  <script>
    const div1 = document.getElementById('div1');
    const div2 = document.getElementById('div2');
    const div3 = document.getElementById('div3');

    div1.addEventListener('click', function (event) {
      alert('点击div1');
    }, true);
    div2.addEventListener('click', function (event) {
      alert('点击div2');
    }, true);
    div3.addEventListener('click', function (event) {
      alert('点击div3');
    }, true);

  </script>
</body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
  • 上面通过设置 addEventListener 第三个参数为 true,让事件在捕获的阶段触发,这样在点击 div3 的时候,会依次弹出 点击div1 > 点击div2 > 点击div3
  • 在捕获阶段,也同样可以通过 event.stopPropagation(); 方法阻止事件的传播。

一般情况下,我们是不需要修改的,除非有特殊需求。

# 8 常用事件

# 鼠标事件

可绑定几乎所有可视元素,如按钮、图片、div、a 标签等。

事件名 描述 示例代码
click 单击(左键) button.addEventListener("click", fn)
dblclick 双击 div.addEventListener("dblclick", fn)
mousedown 鼠标按下 div.addEventListener("mousedown", fn)
mouseup 鼠标释放
mousemove 鼠标移动 用于绘图、拖动
mouseenter 鼠标进入(不冒泡) 类似 mouseover,但不涉及子元素
mouseleave 鼠标离开(不冒泡)
mouseover 鼠标移入(会冒泡) 用于提示、浮层
mouseout 鼠标移出(会冒泡)
contextmenu 鼠标右键 可自定义右键菜单

举个栗子:

document.querySelector("button").addEventListener("click", () => {
  alert("按钮被点击了");
});
1
2
3

# 键盘事件

默认可以绑定可以获取到焦点的元素,例如<input><textarea><button>documentwindow 对象。如果是非表单元素,例如 div,需添加 tabindex="0" 属性才可以获得焦点,然后可以接收键盘事件。

事件名 描述 注意事项
keydown 按键按下(包括功能键) 最常用,支持组合键
keyup 按键释放 可用于监听输入完成

举个栗子:

document.addEventListener("keydown", (e) => {
  console.log(e.key + ", " + e.keyCode);  // 按下按键会打印按键名字和按键的号码
});

document.addEventListener("keydown", (e) => {
  if (e.ctrlKey && e.key === "s") {  // 同时按下ctrl+s
    e.preventDefault();
    alert("保存命令被拦截");
  }
});
1
2
3
4
5
6
7
8
9
10

# 表单事件

可绑定表单类元素,如 <form><input><textarea><select>

事件名 描述 注意事项
submit 表单提交 必须阻止默认提交:e.preventDefault()
reset 表单重置
change 内容改变(失去焦点时才触发 比如输入框输入后点到别处才触发
input 实时输入(每输入一次就触发一次 用于动态校验、计数
focus 获取焦点 不会冒泡
blur 失去焦点 不会冒泡

举个栗子:

阻止表单提交:

document.querySelector("form").addEventListener("submit", function (e) {
  e.preventDefault();
  alert("提交被拦截");
});
1
2
3
4

# 窗口与文档事件

可绑定元素:windowdocument

事件名 描述
load 页面资源(包括图片)全部加载完
DOMContentLoaded 仅 DOM 结构加载完成(推荐)
resize 浏览器窗口大小变化
scroll 页面或元素滚动

举个例子:

window.addEventListener("scroll", function (e) {
  console.log("滚动")
});
1
2
3

# 剪贴板事件

可绑定任何可编辑元素,如 inputtextarea,或使用 contenteditable="true" 的元素

事件名 描述
copy 复制时触发
cut 剪切时触发
paste 粘贴时触发

举个栗子:

禁止粘贴:

input.addEventListener("paste", (e) => {
  e.preventDefault();
  alert("禁止粘贴!");
});
1
2
3
4


下面来做两个练习。

# 16.13 学生信息表

实现一个能添加和删除学生信息的表格,显示如下:

实现思路:

给添加按钮绑定点击事件,点击按钮的时候,获取到姓名和年龄,然后创建 <tr> 元素添加到表格的 <tbody> 中即可。 <tr> 元素中需要创建三个 <td>,在第三个 <td> 中添加删除按钮,并绑定点击事件,用于删除 <tr>

完整代码如下:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>学生信息表</title>
  <style>
    table, th, td {
      border: 1px solid black;
      border-collapse: collapse;
    }
    table {
      margin-top: 10px;
      min-width: 600px;
    }
    th, td {
      padding: 5px 10px;
    }
  </style>
</head>
<body>

  <h2>学生信息表</h2>
  姓名:<input type="text" id="name" />
  年龄:<input type="number" id="age" />
  <button id="addBtn">添加</button>

  <table id="studentTable">
    <thead>
      <tr>
        <th>姓名</th>
        <th>年龄</th>
        <th>操作</th>
      </tr>
    </thead>
    <tbody>
      <!-- 动态插入的学生行在这里 -->
    </tbody>
  </table>

  <script>
    // 名字输入框
    const nameInput = document.getElementById("name");
    // 年龄输入框
    const ageInput = document.getElementById("age");
    // 添加按钮
    const addBtn = document.getElementById("addBtn");
    // 表格的body
    const tbody = document.querySelector("#studentTable tbody");

    // 给添加按钮绑定点击事件
    addBtn.addEventListener("click", function () {
      // 获取到输入框的内容,并去掉前后的空格
      const name = nameInput.value.trim();
      const age = ageInput.value.trim();

      if (!name || !age) {
        alert("请输入完整信息!");
        return;
      }

      // 创建一行tr
      const tr = document.createElement("tr");

      // 创建姓名单元格
      const tdName = document.createElement("td");
      tdName.textContent = name;

      // 创建年龄单元格
      const tdAge = document.createElement("td");
      tdAge.textContent = age;

      // 创建操作单元格和删除按钮
      const tdOp = document.createElement("td");
      const delBtn = document.createElement("button");
      delBtn.textContent = "删除";
      // 给按钮添加绑定事件
      delBtn.addEventListener("click", () => {
        tbody.removeChild(tr);  // 删除tr
      });
      tdOp.appendChild(delBtn);

      // 添加单元格到行中
      tr.appendChild(tdName);
      tr.appendChild(tdAge);
      tr.appendChild(tdOp);

      // 添加行到表格中
      tbody.appendChild(tr);

      // 清空输入框
      nameInput.value = "";
      ageInput.value = "";
    });
  </script>

</body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97

# 16.14 拖拽

创建一个 div,在页面上可以随意拖拽。

如下图:

实现思路:

  1. 当鼠标按下时,记录鼠标相对于元素的偏移量,在移动的时候,设置鼠标也是在元素的相同位置;

  2. 鼠标移动,元素跟随鼠标移动;

  3. 鼠标抬起时,完成拖拽。

代码如下:

<!DOCTYPE html>
<html lang="zh">
<head>
  <meta charset="UTF-8">
  <title>拖拽练习</title>
  <style>
    #drag-box {
      width: 100px;
      height: 100px;
      background-color: lightblue;
      position: absolute; /* 关键 */
      top: 100px;
      left: 100px;
    }
  </style>
</head>
<body>
  <div id="drag-box">拖我</div>

  <script>
    const box = document.getElementById('drag-box');
    let isDragging = false;
    let offsetX = 0;
    let offsetY = 0;

    box.addEventListener('mousedown', function (e) {
      isDragging = true;
      // 计算鼠标点击位置相对 div 的偏移,用于拖拽的准确位置
      offsetX = e.clientX - box.offsetLeft;
      offsetY = e.clientY - box.offsetTop;
      // 防止选中文字
      e.preventDefault();
    });

    document.addEventListener('mousemove', function (e) {
      if (isDragging) {
        // 实时修改div的位置
        box.style.left = (e.clientX - offsetX) + 'px';
        box.style.top = (e.clientY - offsetY) + 'px';
      }
    });

    document.addEventListener('mouseup', function () {
      isDragging = false;
    });
  </script>
</body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48