主题
2-13DOM节点操作
本页对应 第二章WebAPIs,共 6 段视频,约 32 分钟。
学习目标
- 能用自己的话说明 DOM 树的结构,并准确区分元素节点、属性节点、文本节点及根节点。
- 能使用
parentNode查询最近一级父节点,并通过控制台验证返回值为元素对象或null。 - 能使用
children、nextElementSibling、previousElementSibling查询子节点和兄弟节点,并说明返回值类型。 - 能使用
createElement创建节点,并通过append、prepend将节点插入父元素的末尾或开头。 - 能使用
remove删除指定节点,并准确区分删除节点与display: none隐藏元素在 DOM 树中的差异。 - 能为移动端元素注册
touchstart、touchmove、touchend事件,并说明各自触发时机及验证效果。
知识点
先分清:ECMAScript 与 Web APIs
为什么需要: 初学者最容易把 document.querySelector、console.log 都当成“JS 语法”,结果一换环境就混乱。先把 JavaScript 的组成分清,后面学习才知道哪些是语言核心,哪些是浏览器提供的能力。
怎么想: JavaScript 由两大部分组成:ECMAScript 和 Web APIs。ECMAScript 规定基础语法核心知识,比如变量、分支语句、循环语句、对象等;Web APIs 是浏览器提供的接口,其中 DOM 操作 HTML 文档,BOM 操作浏览器窗口。转写稿里反复讲的 DOM 节点、父节点、子节点、增加删除节点,都是 Web APIs 中 DOM 的具体体现,不是 ECMAScript 语法本身。
怎么做: 先记住组成图,再在学习时给每个知识点归类:写 if、for、对象,是 ECMAScript;写 document.querySelector、parentNode、append,是 DOM API。

易错点: 不要把 DOM、BOM 说成 ECMAScript 的一部分;也不要把所有页面效果都归功于“JS 语法”,很多效果依赖浏览器 API。
ECMAScript:变量、分支、循环、对象
为什么需要: 没有 ECMAScript,就无法保存数据、做判断、重复执行逻辑,也无法组织复杂数据。它是写任何 JS 代码的地基。
怎么想: 先学“语言核心”,再学“操作页面”。变量负责存数据,分支负责按条件走不同路径,循环负责重复处理,对象负责把相关数据组织在一起。
怎么做: 例如用变量保存元素,用 if 判断状态,用 for 遍历列表,用对象描述一条评论数据。转写稿中“先获取元素,再注册事件,再打印验证”的流程,底层也离不开变量和函数。
易错点: 只背 API 不练语法,遇到逻辑判断就写不下去;反过来只学语法不学 API,又做不出页面交互。两者要配合。
DOM:把 HTML 文档解析成 DOM 树
为什么需要: 要操作页面标签,先要理解标签之间的关系。DOM 把 HTML 文档以树状结构直观表现出来,根节点之下有 head、body,它们又有子节点,这样标签关系就清楚了。
怎么想: DOM 树中的每一个点都叫节点。节点包括文档节点、元素节点、文本节点、属性节点等,重点要掌握元素节点,因为后续主要对标签做增删改查。转写稿中用“亲爸爸”“兄弟”来类比父子兄关系,就是为了让查找更直观。
怎么做: 先用 document.querySelector 拿到元素,再通过节点关系查找。例如:
js
const ul = document.querySelector('ul')
console.log(ul.children) // 获取所有子元素,返回伪数组
console.log(ul.children[0]) // 第一个子元素查找父节点用 parentNode,查找下一个元素兄弟用 nextElementSibling,上一个用 previousElementSibling。转写稿调试时先 console.log 打印 parentNode,在控制台看到返回的是元素对象;打印 children 看到伪数组里有四个 li,说明关系查找生效。
易错点: parentNode 取最近一级父节点,想取爷爷要写两次;children 返回伪数组,不是真数组;nextElementSibling、previousElementSibling 找的是元素兄弟,不要和文本节点混淆。
DOM 操作:查找、增加、删除节点
为什么需要: 页面中的评论发布、关闭提示框、动态插入内容,都需要在运行时增加或删除节点。只会查找不够,还要会改 DOM 树。
怎么想: 查找节点利用父子兄关系,减少重复 querySelector。增加节点分两步:先创建节点,再插入到父元素中。删除节点则直接调用 remove。
怎么做: 创建用 document.createElement('li'),插入用 append 放到父元素最后,用 prepend 放到最前;删除用 元素.remove()。例如:
js
const ul = document.querySelector('ul')
const li = document.createElement('li')
li.innerHTML = '我是放到后面的'
ul.append(li)
const firstLi = document.createElement('li')
firstLi.innerHTML = '我是放到前面的'
ul.prepend(firstLi)
const removeBox = document.querySelector('.remove')
removeBox.remove()转写稿调试时,创建 li 后先打印,发现页面没有显示,因为只创建还没插入;调用 append 后页面才多出 li。删除时用 remove 后检查 DOM 结构,发现标签确实从 DOM 树中消失。
易错点: append 类似 CSS 的 after,放最后;prepend 类似 before,放最前;创建后不插入页面就看不见;remove 是从 DOM 树删除,和 display: none 隐藏不同,隐藏后标签仍在 DOM 树中。
事件与 BOM:接口要按环境理解
为什么需要: 页面交互离不开事件。移动端没有鼠标,常用手指触摸,因此有 touchstart、touchmove、touchend。BOM 则负责操作浏览器窗口,和 DOM 操作 HTML 文档要区分开。
怎么想: 事件注册属于 Web APIs 的常见用法;BOM 按笔记定义理解即可——它定义了一套操作浏览器窗口的 API,不要和 DOM 混为一谈。
怎么做: 转写稿中用 addEventListener 注册触摸事件:
js
const box = document.querySelector('.box')
box.addEventListener('touchstart', function () {
console.log('我开始摸了')
})调试时在控制台观察:手指触摸触发 touchstart,滑动触发 touchmove,离开触发 touchend。
易错点: 事件名要写准:touchstart、touchmove、touchend;addEventListener 不要拼错;BOM 管浏览器窗口,DOM 管文档内容,二者不要混淆。
先说明:本节笔记与转写稿的主题差异
本节笔记的主题是 window 对象 / BOM,核心是“window 是浏览器中的全局对象、顶级对象”。但提供的转写稿实际讲的是 DOM 节点、父子兄节点查询、增加/删除节点、移动端触摸事件,并不是 window 对象的推导过程。为避免把 DOM 知识硬说成 window 知识,下面只围绕笔记本身扩写;转写稿中能纠正的同音字先说明:do节点/盗目节点应为 DOM 节点,盗目树/dom目树应为 DOM 树,AT码应为 HTML,che要准应为 children,preverse element sibling应为 previousElementSibling,nest element sibling应为 nextElementSibling,M端应为 移动端。这些内容属于 DOM 与移动端事件,不在本节 window 知识点里强行展开。

BOM 与 window:为什么需要一个“总入口”
为什么需要:浏览器给 JavaScript 提供了很多能力,例如访问页面文档、弹窗、控制台输出。这些能力需要一个统一入口,否则语言本身无法直接操作浏览器。怎么想:BOM 是 Browser Object Model,即浏览器对象模型;window 是这个模型中的全局对象,也可以理解为 JavaScript 中的顶级对象。怎么做:在浏览器环境里,很多能力都从 window 出发,例如 window.document、window.alert()、window.console.log()。易错点:不要把 window 等同于 DOM。DOM 主要描述文档结构,document 只是 window 的一个属性;window 的范围更大,包含浏览器窗口相关能力。
document、alert、console.log 都是 window 的属性
为什么需要:理解“省略 window”的写法,必须先知道这些常见对象和方法挂在谁身上。怎么想:笔记指出,document、alert()、console.log() 都是 window 的属性,基本 BOM 的属性和方法都是 window 的。怎么做:可以在控制台验证:
js
console.log(window.document === document); // true
window.alert('hello');
window.console.log('log');易错点:document 不是 window 本身,而是 window 的属性;alert 不是 document 的方法。写 document.alert() 是错误方向。
var 定义在全局作用域的变量、函数会变成 window 的属性和方法
为什么需要:这是早期 JavaScript 全局作用域的重要特征,读旧代码时经常遇到。怎么想:通过 var 在全局作用域声明的变量,以及通过函数声明定义的全局函数,会成为 window 对象的属性和方法。怎么做:
js
var site = 'demo';
function say() {
console.log('say');
}
console.log(window.site); // 'demo'
console.log(window.say === say); // true易错点:let、const 声明的全局变量不会以同样方式挂到 window 上;不要因为 window.site 能访问,就认为所有全局变量都会成为 window 属性。
调用 window 下的属性和方法时可以省略 window
为什么需要:浏览器环境里大量代码直接写 alert()、console.log()、document.querySelector(),需要知道它们为什么能运行。怎么想:因为这些属性和方法本来就在 window 下,所以调用时可以省略 window.。怎么做:
js
window.alert('提示');
alert('提示'); // 等价写法
window.console.log('日志');
console.log('日志'); // 等价写法易错点:省略 window 不等于这些方法“凭空存在”;如果局部作用域中声明了同名变量,可能发生遮蔽。读代码时要能还原成 window.xxx 来理解来源。转写稿里大量出现 document.querySelector(...),也可从这个角度理解为:DOM 操作通过 window 下的 document 入口展开,但转写稿具体讲的父子兄查询、增删节点和触摸事件属于后续 DOM/事件内容,不能当作本节 window 对象的推导。
课件图示








代码示例
示例 1:查找节点(父子兄)+ window 顶级对象
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>查找节点</title>
<style>
.pop {
position: relative;
width: 200px;
padding: 20px;
border: 1px solid #ccc;
}
.close-btn {
position: absolute;
right: 10px;
top: 10px;
cursor: pointer;
text-decoration: none;
}
ul {
list-style: none;
padding: 0;
}
li {
height: 30px;
line-height: 30px;
margin-bottom: 5px;
background: #eee;
}
</style>
</head>
<body>
<!-- 王者荣耀提示框案例 -->
<div class="pop">
王者荣耀
<a href="javascript:;" class="close-btn">×</a>
</div>
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
</ul>
<script>
// 1. 查找父节点:parentNode(最近一级的父节点)
const closeBtn = document.querySelector('.close-btn')
// console.log(closeBtn.parentNode) // 拿到 .pop 这个爸爸
closeBtn.addEventListener('click', function () {
// this 指向 closeBtn,this.parentNode 就是 .pop
this.parentNode.style.display = 'none'
})
// 2. 查找子节点:children(返回伪数组)
const ul = document.querySelector('ul')
console.log(ul.children) // 4 个 li 组成的伪数组
console.log(ul.children[0]) // 第一个 li
console.log(ul.children[2]) // 第三个 li
// 3. 查找兄弟节点
const li2 = document.querySelector('ul li:nth-child(2)')
console.log(li2.previousElementSibling) // 上一个兄弟:第一个 li
console.log(li2.nextElementSibling) // 下一个兄弟:第三个 li
// 4. window 是顶级对象
var num = 10
console.log(window.num) // 10
console.log(window.document === document) // true
</script>
</body>
</html>关键行与易错点
closeBtn.parentNode:只返回最近一级的父节点(亲爸爸),要拿爷爷得写parentNode.parentNode;找不到时返回null。- 回调里用
this代替重复querySelector:this.parentNode就是那个要关闭的爸爸,省去再查一次 DOM。 children返回的是伪数组,没有forEach之外的数组方法不要乱用,取值靠索引children[0]、children[2]。previousElementSibling/nextElementSibling只算元素节点,不会把文本节点算进去;名字很长,别漏写Element。parentNode、children返回的仍然是元素对象,不是字符串,别拿去和字符串比较。var声明在全局作用域的变量会变成window的属性,document、alert、console.log本身就是window的属性和方法,调用时可省略window.。
示例 2:增加节点 + 删除节点
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>增加与删除节点</title>
<style>
ul {
list-style: none;
padding: 0;
}
li {
height: 30px;
line-height: 30px;
margin-bottom: 5px;
background: #eee;
}
.remove {
width: 100px;
height: 50px;
background: pink;
}
.none {
width: 100px;
height: 50px;
background: skyblue;
}
</style>
</head>
<body>
<ul>
<li>我是原有的 li</li>
</ul>
<div class="remove">要被删除的 div</div>
<div class="none">要被隐藏的 div</div>
<script>
// 追加一定要先有父元素
const ul = document.querySelector('ul')
// 第一步:创建节点
const li = document.createElement('li')
li.innerHTML = '我是放到后面的'
// 此时页面上看不到它,因为它还没进入 DOM 树
// 第二步:追加给父元素(放到最后面,类似 CSS 的 ::after)
ul.append(li)
// 放到最前面(类似 CSS 的 ::before)
const firstLi = document.createElement('li')
firstLi.innerHTML = '我是放到前面的'
ul.prepend(firstLi)
// 删除节点:直接从 DOM 树中删掉
const removeBox = document.querySelector('.remove')
removeBox.remove()
// 对比:隐藏节点,标签还在 DOM 树里
const noneBox = document.querySelector('.none')
noneBox.style.display = 'none'
</script>
</body>
</html>关键行与易错点
document.createElement('li'):括号里写字符串形式的标签名,写createElement(li)会报错;创建完页面中看不见,必须追加到父元素里才显示。ul.append(li)/ul.prepend(firstLi)都是方法,别忘加小括号并传入节点;老写法对应appendChild()。append放最后、prepend放最前,可以分别对照 CSS 的::after、::before记忆。remove()是从 DOM 树中删除,删完再用querySelector也找不回来;display: none只是隐藏,标签仍在 DOM 树中。- 内容要在创建节点后、追加前写入(如
li.innerHTML = '...'),先追加再写也可以,但顺序上先写好更清晰。
示例 3:移动端触摸事件
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>移动端触摸事件</title>
<style>
.box {
width: 200px;
height: 200px;
margin: 50px auto;
background: pink;
}
</style>
</head>
<body>
<div class="box"></div>
<script>
const box = document.querySelector('.box')
// 1. 手指触摸开始(类似 PC 端的 click)
box.addEventListener('touchstart', function () {
console.log('我开始摸了')
})
// 2. 手指在元素上滑动,会连续触发
box.addEventListener('touchmove', function () {
console.log('我一直摸')
})
// 3. 手指离开元素
box.addEventListener('touchend', function () {
console.log('我摸完了')
})
</script>
</body>
</html>关键行与易错点
- 移动端不用鼠标用手指,所以事件是
touchstart、touchmove、touchend三个,事件名全是小写、不带on。 - 必须用
addEventListener('touchstart', fn)注册,不要写成box.onclick或box.touchstart = fn。 touchmove只要手指不松开就一直触发,会执行很多次,实际项目里要小心在里面写耗性能的逻辑。- 电脑上用鼠标点不会触发这些事件,要在浏览器的手机模拟器(F12 切换设备)或真机上验证。
常见错误
1. 把 children 当数组用
- 现象:
ul.children.forEach(...)或ul.children.map(...)直接报xxx is not a function。 - 原因:
children返回的是HTMLCollection(类数组),只有length和索引,没有数组方法。 - 修法:先转成真数组再操作,
[...ul.children]或Array.from(ul.children);或直接用for循环按索引取。
2. 兄弟节点拿到一堆空白文本
- 现象:
li.nextSibling打印出来是#text,不是下一个li。 - 原因:
nextSibling/previousSibling找的是所有节点,包括换行缩进产生的文本节点。 - 修法:一律改用
nextElementSibling/previousElementSibling,只找元素节点。
3. 想取"爷爷",结果还是爸爸
- 现象:
a.parentNode打印出来还是父级那一层,以为属性失效。 - 原因:
parentNode只返回最近一级父节点,不会向上递归。 - 修法:需要几层就点几次,
a.parentNode.parentNode;层级深时改用closest('选择器')。
4. 创建了节点,页面上却没有
- 现象:
document.createElement('li')后console.log能看到元素,页面无变化。 - 原因:
createElement只在内存里造了个游离节点,还没进入 DOM 树。 - 修法:必须再执行插入,
父元素.append(新节点)或父元素.prepend(新节点)。
5. 插入位置搞反
- 现象:想让最新评论出现在顶部,结果跑到列表最后。
- 原因:
append是插到父元素内容的末尾,prepend才是插到最前面。 - 修法:结合 CSS 伪元素记忆——
append对应::after,prepend对应::before。
6. 把 remove() 当成隐藏
- 现象:删掉之后又想在别处取回该元素,
querySelector返回null,报Cannot read properties of null。 - 原因:
remove()是把节点从 DOM 树中真正摘除,display:none只是不渲染但节点仍在。 - 修法:只是临时不显示用
style.display = 'none'或加类名;确定不再需要才用remove()。
7. 在 PC 浏览器用鼠标测触摸事件
- 现象:注册了
touchstart,鼠标怎么点都没反应。 - 原因:触摸事件只在支持触摸的设备/模拟环境下触发,鼠标点击不会派发
touch*。 - 修法:用开发者工具的设备模拟(切换到移动端模式并开启触摸模拟),或用真机调试。
练习
练习 1|纯节点关系改写 给定结构 <ul><li>1</li><li>2</li><li class="cur">3</li><li>4</li></ul>,只用节点关系属性(禁止使用任何 querySelector),分别拿到第 1 个 li、第 4 个 li、ul 本身,并打印验证。
练习 2|关闭提示框 还原笔记中的"王者荣耀提示框"案例:页面有 .pop 容器和其中的 .close-btn。要求只查询一次元素,点击关闭按钮后隐藏整个提示框,并在点击回调中用 this 完成定位。写出完整代码。
练习 3|评论发布(置顶) 页面上有输入框、发布按钮和 <ul class="list">。点击发布时读取输入内容,新建 li 插入到列表最前面,内容为空时给出提示且不插入。要求写出创建与插入两步。
练习 4|删除 vs 隐藏对比实验 页面放两个相同的 div,一个用 remove() 处理,一个用 display:none 处理。处理后分别在控制台用 document.querySelector 查找二者,说明结果差异并解释原因。
练习 5|移动端触摸事件日志 一个 200×200 的粉色方块,分别绑定三个触摸事件,把事件名打印到页面上(不要只用 console.log,要能看到滚动记录)。要求在设备模拟器下验证:按下只触发一次开始、滑动连续触发、抬起触发结束。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | DOM节点 | 05:00 |
| 2 | 查询父节点 | 04:46 |
| 3 | 查询子节点和兄弟节点 | 06:30 |
| 4 | 增加节点 | 08:25 |
| 5 | 删除节点 | 03:50 |
| 6 | M端触摸事件 | 04:11 |
小结
- 节点查找的本质是"复用已有元素对象",一次查询就能顺着父子兄关系走遍整棵树,能显著减少重复的
querySelector。 - 查找类 API 返回的都是对象或类数组,不是字符串;只要返回值是
HTMLCollection,就别忘了转数组再用数组方法。 - 兄弟节点一律用
Element版本(nextElementSibling/previousElementSibling),带Element的才是元素,不带的是任意节点。 - 增删改查是 DOM 操作的四件套,增加节点的固定套路永远是"先
createElement,再append/prepend",缺一不可。 - 插入位置的选择对应 CSS 的
::before/::after语义,用这套类比记忆最不容易搞反。 remove()与display:none是两回事:前者改结构,后者改渲染,选错会导致后续查询拿到null。- 移动端不用鼠标而用手指,事件体系从
click切换到touchstart/touchmove/touchend,且必须在触摸环境下验证。 window是顶层对象,document、alert等都挂在它下面,调用时可省略window.;但只有var声明的全局变量才会成为window的属性,let/const不会。