Skip to content

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。

JavaScript组成结构图

易错点: 不要把 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 知识点里强行展开。

window对象与BOM示意图

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 对象的推导。

课件图示 ​

课件第7页:总结 总结 1. JavaScript有几部分组成的? Ø ECMAScript

课件第6页:1.1 BOM l BOM (Browser Object Model ) 是浏

课件第5页:JavaScript的组成 Ø ECMAScript: 规定了js基础语法核心知

课件第9页:1.2 定时器-延迟函数 l JavaScript 内置的一个用来让代码延迟执行

课件第21页:1.6 本地存储分类- localStorage(重点) l 作用: 数据可以长

课件第27页:1.6 本地存储分类- localStorage l 解决:需要将复杂数据类型转

课件第29页:1.6 本地存储分类- localStorage l 解决:把取出来的字符串转换

课件第30页:1.6 本地存储分类- localStorage l 解决方案: 对象 JSON

代码示例 ​

示例 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,要能看到滚动记录)。要求在设备模拟器下验证:按下只触发一次开始、滑动连续触发、抬起触发结束。

原视频对照 ​

#视频时长
1DOM节点05:00
2查询父节点04:46
3查询子节点和兄弟节点06:30
4增加节点08:25
5删除节点03:50
6M端触摸事件04:11

小结 ​

  1. 节点查找的本质是"复用已有元素对象",一次查询就能顺着父子兄关系走遍整棵树,能显著减少重复的 querySelector。
  2. 查找类 API 返回的都是对象或类数组,不是字符串;只要返回值是 HTMLCollection,就别忘了转数组再用数组方法。
  3. 兄弟节点一律用 Element 版本(nextElementSibling/previousElementSibling),带 Element 的才是元素,不带的是任意节点。
  4. 增删改查是 DOM 操作的四件套,增加节点的固定套路永远是"先 createElement,再 append/prepend",缺一不可。
  5. 插入位置的选择对应 CSS 的 ::before/::after 语义,用这套类比记忆最不容易搞反。
  6. remove() 与 display:none 是两回事:前者改结构,后者改渲染,选错会导致后续查询拿到 null。
  7. 移动端不用鼠标而用手指,事件体系从 click 切换到 touchstart/touchmove/touchend,且必须在触摸环境下验证。
  8. window 是顶层对象,document、alert 等都挂在它下面,调用时可省略 window.;但只有 var 声明的全局变量才会成为 window 的属性,let/const 不会。