主题
HTML DOM与事件(Day14 2/2)
本页对应 Day14_DOM,共 6 段视频,约 1 小时 35 分。
学习目标
- 能够使用
table.rows、insertRow()、deleteRow()、tr.rowIndex、tr.cells、insertCell()、deleteCell()、cell.cellIndex完成表格行与单元格的增删,并为删除按钮绑定删除所在行的功能。 - 能够使用
new Image()创建图片元素,设置src、width、height,并通过appendChild()将其插入页面。 - 能够分别使用 HTML 标签属性、DOM 元素对象属性、
addEventListener()三种方式监听事件,并说明同一事件多次监听时三者的生效差异。 - 能够分别解除三种方式绑定的事件监听,并正确使用
removeEventListener()及同一函数引用解除通过addEventListener()绑定的事件。 - 能够描述事件捕获、目标、冒泡三个阶段的触发过程,并说明事件回调函数默认在冒泡阶段执行以及
addEventListener()第三个参数对执行阶段的影响。 - 能够说明事件回调函数中
this指向被监听元素,并使用this解决循环为多个元素绑定事件时的索引错乱问题。
知识点
表格元素的专用方法:table、tr、td
为什么需要:用 createElement 创建 tr、td 再 appendChild 也能做表格,但代码多、层级容易写错。DOM 给表格元素提供了专用方法,能把“创建并插入”合成一步。
怎么想:先分清三层:table 管行,tr 管单元格,td/th 只记自己在行内的位置。
怎么做:table.rows 获取所有行集合;table.insertRow(index) 添加一行,table.deleteRow(index) 删除一行;tr.rowIndex 是本行索引,tr.cells 是本行单元格集合;tr.insertCell(index) 添加单元格,tr.deleteCell(index) 删除单元格;td.cellIndex 是同一行内的索引。
易错点:insertRow 返回新创建的 tr,insertCell 返回新创建的 td,要接住返回值再设内容;不传索引通常加到末尾;索引从 0 开始,且表头行和 tbody 里的行会一起参与 rowIndex 排序。
js
const table = document.querySelector('#table');
const nameInp = document.querySelector('#name');
const addressInp = document.querySelector('#address');
const phoneInp = document.querySelector('#phone');
const btn = document.querySelector('#btn');
btn.onclick = function () {
const tr = table.insertRow(); // 返回新行
tr.insertCell().innerHTML = tr.rowIndex;
tr.insertCell().innerHTML = nameInp.value;
tr.insertCell().innerHTML = addressInp.value;
tr.insertCell().innerHTML = phoneInp.value;
tr.insertCell().innerHTML = '<button class="deleteBtn">删除</button>';
};删除时要拿“当前按钮所在行”的索引。调试中容易先想到按钮集合的 index,但删一行后按钮集合与行集合就对不上了。可靠做法是从按钮往上找:btn.parentElement.parentElement 得到 tr,再用 tr.rowIndex:
js
document.querySelectorAll('.deleteBtn').forEach(btn => {
btn.onclick = function () {
const tr = this.parentElement.parentElement;
table.deleteRow(tr.rowIndex);
};
});快速创建图片元素
为什么需要:图片很常用,new Image() 比 createElement('img') 更直接。
怎么想:它创建出来的仍是图片元素,设置 src、宽高后还要挂到页面上才可见。
怎么做:
js
const img = new Image();
img.src = 'xxx.jpg';
img.width = 200;
img.height = 100;
document.body.appendChild(img);易错点:width/height 这里成为元素自带属性,不等同于写 CSS 样式;不 appendChild 就不会显示。
三种事件监听方式及区别
为什么需要:交互要等用户点击、右击、按键等动作发生后再执行代码,而不是页面一加载就执行。
怎么想:把事件理解成“在某个元素上埋监听,用户触发动作时执行回调”。
怎么做:第一种写 HTML 属性:onclick="...";第二种写元素对象属性:btn.onclick = function(){};第三种用标准方法:btn.addEventListener('click', fn)。
易错点:前两种同一个事件写多次,前者只有前面的生效,后者后面的覆盖前面的;addEventListener 事件名不带 on,同一事件可绑定多个回调,都会执行。多人协作时更推荐第三种,避免互相覆盖。
js
btn1.onclick = function () { console.log('btn1'); };
btn2.onclick = function () { console.log('btn2'); };
btn3.addEventListener('click', function () { console.log('btn3-a'); });
btn3.addEventListener('click', function () { console.log('btn3-b'); });解除事件监听
为什么需要:一次性按钮、广告按钮等场景,触发后要取消监听。
怎么想:前两种本质是把函数赋给元素属性,解除时用空值覆盖;第三种必须移除“同一个函数引用”。
怎么做:
js
btn1.onclick = null;
btn2.onclick = null;
function handler() { console.log('btn3'); }
btn3.addEventListener('click', handler);
btn3.removeEventListener('click', handler);易错点:removeEventListener 不能传一个临时匿名函数,必须传添加时的同一个函数;回调函数加括号会立即执行,传参时不能加 ()。
事件流:捕获、目标、冒泡
为什么需要:父子元素都监听点击时,必须知道谁先触发。
怎么想:用户点某个元素后,浏览器先从文档层层向下找到目标元素,这叫捕获;找到目标元素是目标阶段;再从目标元素层层向上回到文档,这叫冒泡。事件回调默认在冒泡阶段执行。
怎么做:addEventListener('click', fn, true) 第三参数为 true 时改为捕获阶段触发;前两种监听方式不能这样调整。
易错点:目标元素不一定监听了事件;捕获和冒泡会经过很多元素,只有刚好监听了事件的元素才执行回调。
回调函数中的 this
为什么需要:一组元素绑定同一逻辑时,要知道“当前点的是谁”。
怎么想:事件回调里的 this 指向监听事件的元素,三种方式结论一致。
怎么做:可用 this 代替循环变量:
js
const lis = document.querySelectorAll('#books li');
lis.forEach(function (li) {
li.onclick = function () {
this.classList.toggle('active');
};
});易错点:用 for 循环绑定事件时,回调里的 i 是全局变量,点击发生时循环早已结束,i 往往变成最后的值,导致报错或全选最后一个;此时用 this 或 forEach 的形参更稳。
表格元素的专用属性与方法
为什么需要:用 createElement('tr')、createElement('td') 再层层 appendChild 也能做表格,但代码长、层级容易写错。表格元素自带专用方法,能把“创建并插入”合成一步。
怎么想:先分清三层:table 管行,tr 管单元格,td/th 只关心自己在行内的位置。table.rows 取所有行;tr.cells 取本行所有单元格。
怎么做:
js
const table = document.querySelector('#table');
const tr = table.insertRow(); // 返回新 tr,默认加到末尾
const td1 = tr.insertCell(); // 返回新 td
td1.innerHTML = tr.rowIndex; // 行索引从 0 开始,表头行也参与删除时,按钮在 td 里,先 parentElement.parentElement 找到 tr,再用 rowIndex:
js
btn.onclick = function () {
const tr = this.parentElement.parentElement;
table.deleteRow(tr.rowIndex);
};易错点:insertRow(0) 不一定插到 tbody 第一行,因为 thead 和 tbody 的 tr 会一起排索引;不传参数才默认加到最后。rowIndex 从 0 开始,表头行也会占一个索引。
快速创建图片元素
为什么需要:创建图片很常用,new Image() 比 createElement('img') 更直接。
怎么想:它创建出来的仍是图片元素,设置 src 后还要挂到页面上才可见。
怎么做:
js
const img = new Image();
img.src = 'demo.png';
img.width = 200;
img.height = 100;
document.body.appendChild(img);易错点:width/height 在这里是元素自带属性,不是 CSS 样式;目前转写稿中明确提到可用 new 创建的特殊元素是 Image,不要随意类推到所有元素。
三种事件监听方式
为什么需要:事件让代码“等用户动作发生后再执行”,而不是页面一加载就执行。要给元素绑定点击、右击、键盘等动作,就需要监听。
怎么想:三种写法本质都是“事件名 + 回调函数”,区别在绑定位置和重复绑定规则。
怎么做:
html
<button id="btn1" onclick="console.log('btn1')">按钮1</button>js
const btn2 = document.querySelector('#btn2');
btn2.onclick = function () { console.log('btn2'); };
const btn3 = document.querySelector('#btn3');
btn3.addEventListener('click', function () { console.log('btn3'); });易错点:第一种把事件写成标签属性,同名事件写两次只有前面生效;第二种写成元素对象属性,同名事件后写覆盖前写;第三种 addEventListener 可对同一事件绑定多个回调,事件名不带 on,写 click 而不是 onclick。
解除事件监听
为什么需要:一次性按钮、广告关闭、权限变化等场景,需要“触发一次后不再触发”。
怎么想:前两种监听本质是元素对象属性,赋空即可覆盖;第三种必须用 removeEventListener,并传入同一个函数引用。
怎么做:
js
btn2.onclick = null;
function handler() { console.log('btn3'); }
btn3.addEventListener('click', handler);
btn3.removeEventListener('click', handler);易错点:removeEventListener 不能传一个“长得一样”的匿名函数,必须是同一个函数引用;所以回调要先起名或存变量。
事件流:捕获、目标、冒泡
为什么需要:点击子元素时,父元素上的事件也可能触发,必须理解触发顺序,才能解释“为什么点了绿色却先执行橙色”。
怎么想:事件从 document 层层向下找到目标元素,叫捕获;找到目标后,再从目标层层向上,叫冒泡。回调默认在冒泡阶段执行。
怎么做:
js
box2.addEventListener('click', function () {
console.log('橙色被点击');
});
box3.addEventListener('click', function () {
console.log('黄色被点击');
});
// 第三个参数 true:改为捕获阶段触发
box2.addEventListener('click', fn, true);易错点:目标元素不一定是绑了事件的元素;捕获和冒泡都会经过祖先元素,只有刚好绑了事件且处于对应阶段,回调才执行。前两种监听方式不能调整阶段,默认走冒泡。
回调函数中的 this
为什么需要:一组 li 都要绑定点击事件,回调执行时要知道“到底点的是哪一个”。
怎么想:事件回调里的 this 指向监听事件的元素。用 forEach 时每个回调有形参,各自独立;用 for 循环时若引用外层 i,点击时循环早已结束,i 已变成最终值。
怎么做:
js
const items = document.querySelectorAll('#book-list li');
items.forEach(function (item) {
item.onclick = function () {
this.classList.toggle('active');
};
});用 for 循环时也可在回调里用 this,而不是用循环变量 i。
易错点:转写稿中的调试现象是:for 循环里回调输出 i 全是 6,因为回调要等点击才执行,那时循环已结束;再用 items[6] 会找不到元素而报错。this 能稳定拿到当前被点击的 li。
课堂画面






代码示例
1. 表格相关元素:insertRow / insertCell / deleteRow
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>表格相关元素</title>
<style>
table { border-collapse: collapse; }
td, th { border: 1px solid #999; padding: 6px 12px; }
.deleteBtn { color: red; }
</style>
</head>
<body>
<input type="text" id="name" placeholder="姓名">
<input type="text" id="address" placeholder="地址">
<input type="text" id="phone" placeholder="电话">
<button id="btn">添加</button>
<table id="table">
<thead>
<tr>
<th>序号</th><th>姓名</th><th>地址</th><th>电话</th><th>操作</th>
</tr>
</thead>
<tbody></tbody>
</table>
<script>
// 1. 获取相关元素
const tableBooks = document.querySelector('#table');
const nameInp = document.querySelector('#name');
const addressInp = document.querySelector('#address');
const phoneInp = document.querySelector('#phone');
const btn = document.querySelector('#btn');
// 2. 点击添加
btn.onclick = function () {
// insertRow() 不传参 → 添加到最后;返回的就是新创建的 TR
const tr = tableBooks.insertRow();
// insertCell() 不传参 → 加到行末;返回值就是新创建的 TD
tr.insertCell().innerHTML = tr.rowIndex; // 行号,从 0 开始,表头是 0
tr.insertCell().innerHTML = nameInp.value;
tr.insertCell().innerHTML = addressInp.value;
tr.insertCell().innerHTML = phoneInp.value;
tr.insertCell().innerHTML = '<a href="javascript:;" class="deleteBtn">删除</a>';
bindDelete(); // 新增的删除按钮也要能点
};
// 3. 给每个删除按钮绑定事件
function bindDelete() {
const deleteBtns = tableBooks.querySelectorAll('.deleteBtn');
deleteBtns.forEach(function (btn) {
btn.onclick = function () {
// 删除按钮 → 父元素 TD → 父元素 TR
const tr = this.parentElement.parentElement;
tableBooks.deleteRow(tr.rowIndex); // 按行索引删除一行
};
});
}
</script>
</body>
</html>要点
table.insertRow()一个方法顶createElement('tr') + appendChild两步,返回值就是新 TR,可以继续操作;TR 的父元素是 tbody,不是 table,所以不能用table.appendChild(tr)。insertRow()的参数是行位置索引。传0会插到 thead 那行前面——因为 thead 的 TR 和 tbody 的 TR 是一起排号的;不传参就加到最后。tr.insertCell()同理,返回新 TD;不传参加到行末,传0会插到最前面,所以写第二个单元格要传1,别再传0。tr.rowIndex是本行索引(表头行是 0,tbody 第一行是 1),不是“第几个删除按钮”。删除一行后按钮集合的下标会错位,所以不能用按钮在集合里的 index 当行索引。- 删除按钮取所在行:
this.parentElement.parentElement拿到 TR,再用rowIndex交给deleteRow()。
2. 三种事件监听方式与解除监听
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>三种事件监听方式与解除监听</title>
<style>
#list { list-style: none; padding: 0; width: 635px; }
#list li { padding: 10px; border-bottom: 1px solid #999; cursor: pointer; }
#list li.active { background: #900; color: #fff; }
</style>
</head>
<body>
<!-- 方式一:事件作为标签的属性,事件名要带 on -->
<button id="btn1" onclick="console.log('btn1 被点击了')">按钮01</button>
<button id="btn2">按钮02</button>
<button id="btn3">按钮03</button>
<button id="btn4">解除事件监听</button>
<ul id="list">
<li>刘备</li>
<li>关羽</li>
<li>张飞</li>
</ul>
<script>
const btn1 = document.querySelector('#btn1');
const btn2 = document.querySelector('#btn2');
const btn3 = document.querySelector('#btn3');
const btn4 = document.querySelector('#btn4');
// 方式二:事件名作为元素对象的属性
btn2.onclick = function () {
console.log('btn2 被点击了', this.id); // this 指向监听事件的元素
};
// 方式三:addEventListener,事件名不带 on,回调必须具名才能解除
function handler() {
console.log('btn3 被点击了', this.id);
}
btn3.addEventListener('click', handler);
// 解除监听
btn4.onclick = function () {
btn1.onclick = null; // 前两种:置空即覆盖掉原回调
btn2.onclick = null;
btn3.removeEventListener('click', handler); // 第三种:必须传同一个函数
};
// this 的应用:给一组 li 绑定点击选中
const items = document.querySelectorAll('#list li');
for (var i = 0; i < items.length; i++) {
items[i].onclick = function () {
this.classList.toggle('active'); // this 就是被点击的那个 li
};
}
</script>
</body>
</html>要点
- 方式一和方式二本质相同(标签属性映射成元素对象的属性),所以解除方式也相同:
元素.onclick = null;同一事件写两遍,方式一只有前面的生效,方式二只有后面的生效(覆盖)。 - 方式三
addEventListener('click', fn)事件名不带 on;同一事件绑定多个回调会全部执行,多人协作时不会互相覆盖。 - 解除第三种监听必须
removeEventListener('click', 同一个函数引用):匿名函数解除不了,写一模一样的函数也不行(函数是对象类型,写一遍就是新的一个),只能先把函数赋给变量/起名字再传进去。回调传函数名时不加小括号,加了就自己调用了。 - 三种方式回调里的
this规则一样:都指向监听事件的那个元素,可以用它直接取到当前元素。 - 在 for 循环里给一组元素绑定事件时,不要在回调里用
items[i]:var声明的 i 是全局变量,回调执行时循环早已结束,i 恒等于 length,会报错。改用this拿当前元素就完全没问题。
3. 事件流:捕获 / 目标 / 冒泡
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>事件流</title>
<style>
#box1 { padding: 100px; width: 800px; background: #f00; }
#box2 { padding: 100px; background: #f80; }
#box3 { padding: 100px; background: #ff0; }
#box4 { height: 100px; background: #080; }
</style>
</head>
<body>
<div id="box1">
<div id="box2">
<div id="box3">
<div id="box4"></div>
</div>
</div>
</div>
<script>
const box2 = document.querySelector('#box2');
const box3 = document.querySelector('#box3');
// 第三个参数为 true → 回调在【捕获阶段】执行
box2.addEventListener('click', function () {
console.log('橙色的元素被点击了');
}, true);
// 不写第三个参数 → 默认 false,回调在【冒泡阶段】执行
box3.addEventListener('click', function () {
console.log('黄色的元素被点击了');
});
</script>
</body>
</html>要点
- 事件触发分三阶段:捕获(从 document 层层向下找目标元素)→ 目标阶段(找到目标元素那一刻,承上启下)→ 冒泡(从目标元素层层向上回到 document)。也有人只分捕获和冒泡两段。
- 点最内层的绿块,相当于也点了黄、橙、红(目标元素是绿块,它没有监听事件也照样是目标);但回调默认只在冒泡阶段执行,所以经过黄、橙时才会打印——先黄后橙。
- 红块没监听事件,捕获和冒泡照样经过它,只是什么都不干;这个过程每次点击都会发生,跟有没有监听事件无关。
addEventListener第三个参数默认为false(冒泡阶段执行),设成true就在捕获阶段执行:所以本例点绿块会先打印橙色、再打印黄色。- 只有
addEventListener能调整执行阶段,onclick和标签属性写法都改不了。
常见错误
- 现象:
table.insertRow(0)后新行跑到表头里 → 原因:把thead和tbody中的tr分开编号了 → 修法:按整张表所有tr的统一索引理解,必要时用table.rows验证位置。 - 现象:连续
insertCell(0)后单元格顺序反了 → 原因:每次都插到第 0 位,后面的被后移 → 修法:按目标顺序使用递增索引,或省略参数追加到末尾。 - 现象:删除按钮点一次后,后续删除错行或报错 → 原因:用按钮集合的
index当行号,删除后集合与行索引错位 → 修法:从当前按钮向上找到所在tr,用该行的rowIndex,或直接用this定位当前行。 - 现象:
removeEventListener解除不了事件 → 原因:添加和移除时传入的不是同一个函数引用,匿名函数每次都是新函数 → 修法:把回调函数保存为具名函数或变量,添加和移除使用同一引用。 - 现象:
for循环给一组元素绑定事件后,点击每个元素都拿到最后一个索引,甚至报错 → 原因:回调执行时循环早已结束,变量是共享的全局变量 → 修法:回调中改用this获取当前监听元素,或用let/闭包保存当次索引。 - 现象:给同一元素同一事件绑定多次,结果与预期不一致 → 原因:三种监听方式对重复绑定的处理不同,前两种会覆盖,第三种可叠加 → 修法:明确需求后统一监听方式;需要多人协作叠加时用
addEventListener,需要覆盖时用属性方式。
练习
- 实现一个表格:输入姓名、地址、电话,点击“添加”用
insertRow/insertCell新增一行,并显示当前行号;每行带“删除”按钮,点击删除当前行。 - 用
new Image()创建一个图片元素,设置src、width、height后挂到body,再对比用createElement('img')的写法。 - 对同一个按钮分别用三种方式监听
click,观察重复绑定、执行顺序和解除监听时的差异,并写出你的选择理由。 - 写一个父子嵌套结构,分别给父、子元素监听
click,通过addEventListener第三个参数验证冒泡与捕获的执行顺序。 - 用
for循环给一组li绑定点击事件,实现点击项高亮;要求分别用this和闭包两种方式修正索引问题。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | HTMLDOM-表格相关元素 | 23:19 |
| 2 | HTMLDOM-快速创建新图片 | 01:57 |
| 3 | 三种事件监听方式 | 18:22 |
| 4 | 解除事件的监听 | 12:51 |
| 5 | 事件流 | 22:26 |
| 6 | 事件回调函数中的this | 16:48 |
小结
- 表格相关 API 的核心是按“表—行—单元格”的层级操作,索引位置要特别谨慎。
- 行索引应优先从当前行元素获取,不要用按钮集合索引代替行索引。
- 动态增删表格时,先定位当前操作对象,再调用对应删除或插入方法。
- 图片可用
new Image()快速创建,但创建后仍需挂载到页面结构才可见。 - 事件监听三种方式都能用,但覆盖规则、解除方式和协作场景不同。
- 需要解除监听时,必须保留并复用同一个回调函数引用。
- 事件回调中的
this指向监听事件的元素,是解决批量绑定定位问题的可靠方式。 - 默认回调在冒泡阶段执行,理解捕获、目标、冒泡顺序有助于排查嵌套元素事件问题。