
DOM核心知识
注意: 本章知识仅适用于Web前端开发学习路线或全栈开发路线,如果仅使用JavaScript做后端开发,不强制要求学习。
前面我们为大家介绍了JS的基础语法部分,这一章我们将继续深入Web浏览器环境,学习然后控制页面上的元素并实现各种高级操作。
元素和属性
在实际开发中,我们经常需要动态控制页面上的元素样式,比如当鼠标点击元素时,我们可以改变它的颜色,点击按钮时可以展示弹窗,用户滑动窗口时实时展示当前滚动的进度。这些效果都需要通过DOM提供的方法来实现,我们可以利用JavaScript与其进行交互,让我们的网站动起来。
DOM介绍
DOM(Document Object Model,文档对象模型)可以理解为:浏览器把 HTML 页面转换成的一棵“对象树”。这棵树里的每一个节点,都是 JavaScript 可以操作的对象。比如下面的代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div>我是页面上的一段普通内容</div>
<p>我是一个段落</p>
</body>
</html>
浏览器会把上面的代码自动转换为下面的树形结构:

在HTML中,这种结构叫做DOM树,我们需要将这个树形结构倒过来看,HTML元素就是这棵树的树根,而它的子元素,实际上就是这棵树上的分支。所有的元素,在浏览器加载页面后,会在内存中生成一个对应的 DOM 节点对象,此时 JavaScript 就可以直接操作这个节点。
在JS中,节点分为很多种类型,一共有14种类型,但其中比较常见的是以下五种类型:
- 元素节点(
ELEMENT_NODE):操作 HTML 元素的核心。 - 文本节点(
TEXT_NODE):处理元素内的文本内容。 - 注释节点(
COMMENT_NODE):有时用于模板标记或调试。 - 文档节点(
DOCUMENT_NODE):全局document对象,用于访问整个页面。 - 文档片段节点(
DOCUMENT_FRAGMENT_NODE):高效批量添加 DOM 元素。
比如我们编写的元素就是一个节点,也就是 HTML 标签本身,它是页面结构的核心组成部分,下面这些都是元素节点:
<div class="container"></div>
<p>Hello World</p>
<a href="https://example.com">链接</a>
而文本节点,就是包含在元素或属性中的纯文本内容,包括空格、换行等空白字符:
<div>
这里是文本节点
<p>Hello <span>World</span></p>
</div>
此外,HTML 中的注释内容,虽然不会在页面中显示,但可以通过 DOM 访问,这些是注释节点:
<!-- 这是一个注释节点 -->
整个文档的根就是文档节点,一般情况下是html标签:
<html lang="en">
...
</html>
几乎在HTML文件中出现的所有内容,都会变成一个JS可以控制的节点,哪怕是注释也会被算在其中。因此,DOM 让 JavaScript 有了“操作页面的能力”。下一节我们将介绍如何通过JS获取DOM上的节点。
获取元素
在操作页面之前,第一步永远是:先找到元素,通过JS查找元素有很多种方式,浏览器为我们提供了多种“查找 DOM 元素”的方式,下面我们从最基础、最容易理解的方法开始,一步步往后讲。
我们需要用到doucment对象,它是整个DOM的总管,代表当前这个HTML文档,我们可以通过它来获取DOM上的任何内容。第一种是通过 id 获取元素(getElementById)这是最常用、也是新手最先学会的方法:
document.getElementById(id)
这里的参数是 元素的 id 名称(字符串),返回值是 一个元素对象,如果找不到元素,则返回 null:
<div id="box">我是一个盒子</div>
const box = document.getElementById('box')
console.log(box)
可以看到,这里得到了一个HTML元素对象,类型是HTMLElement,HTML中所有的元素类型都是Node的子类,继承关系和顺序如下:
- EventTarget:最顶层的基类,让对象拥有处理事件的能力(比如
addEventListener)。 - Node:基础的节点类,DOM 树中的所有内容(标签、文本、注释等)都是 Node。
- Element:更具体的节点,它专门指代“标签”,提供了处理属性(Attribute)的方法。
- HTMLElement:HTML 专用的元素,它包含了所有 HTML 标签共有的属性(比如
id,className,style)。 - 具体标签(如
HTMLDivElement):特定标签的属性(比如<a>标签独有的href)
我们如果直接打印它,会以HTML代码的形式在控制台展示:

我们可以通过nodeType来获取它的具体类型:
console.log(box.nodeType === Node.ELEMENT_NODE)
由于得到的结果是一个数字,我们直接使用Node构造函数中提供的常量来进行比较即可。
当然,我们也可以在一个单独的JS文件中编写这段代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="js/script.js"></script>
</head>
//script.js
const box = document.getElementById('box')
console.log(box)
此时我们会发现,这样好像不太行,控制台中获取到的元素是一个null:

这其实是因为我们的JS引入是写在head标签内的,而页面内容是在body中,网页的加载顺序是从上往下进行的,当JS加载的时候,页面内容还未渲染完成(DOM树还未完成构建)所以就无法拿到需要的元素了。
这里推荐几种做法,最简单的就是将 <script> 标签移到 </body> 结束标签之前:
<body>
<div>我是页面上的一段普通内容</div>
<div id="box">我是一个盒子</div>
<!-- 放在最后执行,此时前面的元素都已经完成加载 -->
<script src="js/script.js"></script>
</body>
还有一种方法是给 <script> 添加 defer 属性,这个属性可以使得JS延迟加载,确保脚本执行时能访问到所有 DOM 元素:
<head>
<meta charset="UTF-8">
<title>Title</title>
<script defer src="js/script.js"></script>
</head>
我们也可以使用getElementsByTagName根据标签名字一次性获取所有元素:
document.getElementsByTagName("div")

这里得到的是一个HTMLCollection,它是一个类数组(array-like)集合对象,包含了一组有序的 DOM 元素,使用方法非常简单:
const elements = document.getElementsByTagName("div")
console.log(elements.item(0)) //使用item访问指定下标的元素
console.log(elements[1]) //也可以和数组一样玩

此外,HTMLCollection还提供了一个namedItem用于快速查找集合中name或id属性为指定值的元素:
console.log(elements.namedItem("box"))
我们也可以使用getElementsByClassName通过 class 名称来获取元素:
const elements = document.getElementsByClassName("item")
console.log(elements)
<div class="item">A</div>
<div class="item">B</div>
我们还可以根据name属性来获取元素,这个方法在现代开发中用得不多,主要用于表单:
<input type="text" name="username">
<input type="password" name="password">
const inputs = document.getElementsByName('username')
除了根据名字属性获取外,document上直接提供了根元素的获取,documentElement直接代表页面根元素,也就是html元素:
document.documentElement

此外,body属性也是直接代表body这个元素:

最后我们来介绍一个最重要的,也是开发中最常用的一种元素获取方式,querySelector可以使用 CSS 选择器获取单个元素:
document.querySelector("#box")
要判断我们的选择器是否使用正确,也可以使用matches进行判断:
box.matches("#box") //当CSS选择器匹配时,返回真
利用这个方法,可以替代前面讲到的全部方法,只要CSS选择器编写合理,就能直接选择需要的元素,非常方便。不过需要注意的是,这个方法只会返回匹配到的第一个元素,如果需要一次性获取所有满足CSS选择器的元素,可以使用querySelectorAll方法:
document.querySelectorAll(".item")
它会返回一个NodeList对象,和我们前面介绍的HTMLCollection不同的是,HTMLCollection 仅包含 **Element(元素节点)**比如 <div>、<p> 等标签。而 NodeList 包含所有 Node(节点)元素,所以还包括文本节点(换行、空格)、注释节点以及属性节点,因为CSS选择器选择的范围更加广泛,所以它应该包含更多种类。它提供了forEach用于快速遍历子节点:
document.querySelectorAll(".item").forEach((value, key, parent) => {
console.log(key, value, parent) //key就是顺序下标,value就是节点本身,parent就是当前这个节点列表
})
此外,还有一个非常重要的是,HTMLCollection是动态的,一旦DOM发生变化,这个对象会跟着变化,而NodeList是获取时产生的静态快照,我们可以尝试在打印对象之后删除页面上的一个元素:

此时HTMLCollection的长度会变化成1,而NodeList无感。
以上讲解的方法,除了在document上使用,我们也可以在任意一个元素上使用,来实现快速查找子元素:
const box = document.getElementById('box')
box.querySelector("p") //直接在这个box元素上查找内部符合条件的子元素
此外,我们还可以使用:
element.closest('.container') //用于向上查找符合选择器的祖先元素
查找到元素之后,我们就可以开始愉快的操控它了。
HTML属性
在前面我们已经学会了如何获取元素,但仅仅拿到元素还不够,真正让页面“动起来”的关键,是读取和修改元素的属性。属性(Attribute),简单理解就是写在 HTML 标签上的那些“信息”,比如:
<div id="box" class="item" title="提示文本"></div>
这里的 id、class、title,都是元素的属性,这是我们在HTML中为大家介绍的。需要获取HTML标签上的属性,我们可以直接使用getAttribute方法:
console.log(element.getAttribute("id"))
注意返回值永远是字符串,因为属性都是直接写的值,如果属性不存在,会返回 null。除了获取之外,我们也可以使用setAttribute来修改某个属性:
element.setAttribute("class", "666")

如果这个属性不存在,那么将会新增这个属性:
box.setAttribute("data-v-25565", "666")

不过,对于一些非标准的HTML标签属性,我们更建议大家使用后续DOM属性中的dataset来实现。
对于一些布尔属性来说,设置不需要填写特别的内容,如果要使用直接设置值为空即可,如果不需要则移除属性,比如:
<input disabled="disabled">
input.removeAttribute('disabled') // 表示 false
input.setAttribute('disabled', '') // 表示 true
这里需要注意的是,HTML 属性名本身不区分大小写,所以即使我们写一个大写的也会变成小写形式,覆盖也会按照小写的形式去进行覆盖:
box.setAttribute("CLASS", "666")

不过,这里还是建议大家在 JavaScript 中调用方法时,属性名统一推荐使用小写,这是业界的通用约定,避免出现兼容性和可读性问题。
有时候我们并不关心属性的值,只关心它有没有被写在标签上,这时可以使用hasAttribute来判断:
element.hasAttribute('title')
我们也可以使用 removeAttribute 删除某个属性:
element.removeAttribute('title')
如果属性本身不存在,不会报错,但也不会有任何效果。
DOM属性
在上一节中我们讲了 HTML 属性(Attribute),它们是写在标签上的信息。而这一节要讲的 DOM 属性(Property),是浏览器在内存中为每一个元素创建的 JavaScript 对象属性。所以,HTML 属性是“写给浏览器看的”,DOM 属性是“写给 JavaScript 用的”。
当浏览器加载 HTML 页面时,会做两件事:
- 解析 HTML 标签上的属性(Attribute)
- 基于这些属性,生成一个对应的 DOM 对象
比如一个input标签:
<input type="text" value="hello">
浏览器会创建一个 HTMLInputElement 对象,其中包含大量 DOM 属性:
input.value
input.type
input.disabled
input.checked
input.placeholder
这些属性并不一定全部写在 HTML 中,但 只要元素类型支持,就一定存在于 DOM 对象上。想要直接观察DOM元素的各个属性,可以直接打开开发者工具,然后再元素界面选择需要观察的元素,接着在下面找到属性:

DOM 属性的访问方式非常直观,就像普通 JS 对象一样,比如我们想要修改元素的id属性,直接对其进行修改即可,修改后会立即生效到元素上:
const box = document.getElementById('box')
box.id = 'crazy'

对于一些非标准HTML属性,我们也可以使用dataset来实现:
box.dataset.role = 'guest'

此外,它还可以实现自动解析驼峰的效果:
console.log(box.dataset.userId) //获取的其实是data-user-id
不过,和前面介绍的 HTML 属性的一个区别是,DOM 属性的值不一定是字符串。比如一些布尔属性,它们的值就是布尔类型的:
console.log(box.draggable) //false
这里我们着重介绍两个比较常用的DOM属性,首先是用于设置class的className和classList属性:
console.log(box.className) //字符串结果
box.className = "crazy item"
修改className后,元素的class属性也会一起跟着变化,但是如果我们需要追加新的类,还需要手动进行拼接,这显然是非常麻烦的,所以我们更推荐大家使用下面的classList属性来修改类,它使用起来更加方便:
console.log(box.classList) //得到一个类列表
这里得到的是一个DOMTokenList对象,它可以实现对所有以空格分割的属性进行快捷编辑,我们可以直接通过它来进行类的增加和删除,直接使用add就可以插入新的类了,就像使用数组那样:
box.classList.add('crazy', 'thursday', 'vivo', '50yuan')

我们也可以使用remove来对属性进行删除:
box.classList.remove("item")

其他方法也很好用:
box.classList.toggle('active') //切换类,如果有则删除,没有就新增
box.classList.toggle('active', true) //切换类,强制设置
box.classList.contains('active') //判断是否包含类
box.classList.replace("item", "crazy") //把一个类替换成另外一个类
我们接着来看如何更改内联样式,使用style即可:
<div id="box" style="background: pink">我是文本</div>
console.log(box.style)
style属性中包含了目前几乎所有可用的CSS属性,需要调整哪个属性,只需要直接对其进行赋值即可。比如我们现在需要修改文本的颜色:
box.style.color = 'white' //等价于 color: white; 白色

此时页面上的元素style属性也会跟着发生变化,每个属性的设置都是独立的,不会影响其他属性。
当然,DOM属性不仅包含HTML标签上的属性,还有一些元素自己本身的属性,比如前面介绍的innerText、nodeType等,其实都是DOM属性。有关其他DOM属性,我们会在后续课程中逐步进行讲解,大家在使用时也可以在MDN文档上自行查询需要的属性。
元素内容
在前面我们已经学习了如何获取元素,以及元素的属性,这一节我们接着来介绍几个比较特殊的属性,它们可以操控元素的内容。在所有的元素对象中,innerHTML 用来 获取或设置元素内部的 HTML 结构:
<div id="box">
<span>hello</span>
</div>
const box = document.getElementById('box')
console.log(box.innerHTML)
// <span>hello</span>
可以看到,innerHTML代表的是元素内部的HTML文档结构,打印出来的值也是一个HTML格式的代码。同样的,既然可以获取,我们也可以使用这个属性来修改内部的HTML代码:
box.innerHTML = '<p>新的内容</p>'

虽然这种做法在修改HTML结构上简单粗暴,但是它存在诸多问题,其中最主要的就是安全和性能问题:
- 当你修改
innerHTML时,浏览器并不会只更新你改动的那一小部分,浏览器必须销毁该元素下所有的旧 DOM 节点,重新解析 HTML 字符串,并创建新的 DOM 对象,大规模的 DOM 变更会触发昂贵的浏览器重排(Reflow)和重绘(Repaint),在循环中使用innerHTML += '...'更是性能自杀。 - 如果你直接把用户输入的数据赋给
innerHTML,就相当于给黑客开了后门,因为客户可以利用这种方式在你的网页上直接插入javascript标签来执行恶意JS代码。 innerHTML对格式要求极严。如果你不小心传入了未闭合的标签(比如<div>文字),浏览器会尝试自动修复,但这往往会导致最终生成的 DOM 结构和你预期的完全不同,甚至引发布局崩坏。- 如果你用
innerHTML覆盖了一个容器,即便你只是改了一行文字,原本绑定在容器内部子元素上的所有 Event Listeners(事件监听器)都会随之消失(后续章节会介绍事件监听机制)
因此,如果实在需要增加删除或是修改元素,我们更推荐使用后续章节中讲到的方法来操作,而不是直接修改HTML内容。如果仅仅只是修改页面上的文字内容,建议使用下面的textContent方式。
除了innerHTML之外,还有innerText,它可以用来获取或设置 “人眼能看到的文本”。
<div id="box">Hello World</div>
或是这种嵌套写法,都只会解析可视文本部分:
<div id="box">
Hello <span>World</span>
</div>
console.log(box.innerText) //Hello World
注意,如果文本内容是不可见的,那么这里得到的结果也会是一个空字符串:
<div id="box" style="visibility: hidden">Hello World</div>
最后,还有一个textContent,它的功能和innerText差不多,也可以用来获取或设置 纯文本内容,但是它不会受到可视性影响:
console.log(box.textContent) //即使不可见依然可以得到正确结果
此外,针对于HTML中的换行也会一起保留下来,而innerText是渲染之后的实际展示文本,不会保留换行:
<div id="box" style="visibility: hidden">Hello
World</div>

相比innerText,它获取内容的性能更高,因为前者需要计算布局(Reflow)来确定文本是否可见。但是,一些特殊标签里面的内容也会被textContent拿到:
<div id="box">
<script>
for (let i = 0; i < 6; i++) {
console.log("孩子们,这并不好笑")
}
</script>
</div>

而innerText则会绕过这些实际不可见的内容。无论使用textContent还是innerText,我们都可以对内容进行修改,但是注意,这里设置的是纯文本内容,也就是普通文本元素:
box.textContent = "卡布奇诺今犹在,不见当年倒茶人"
box.innerText = "卡布奇诺今犹在,不见当年倒茶人"

注意,由于这里仅仅只是文本内容设置,所以即使我们设置一段HTML代码,也会被自动转义为文本形式的内容,变成普通文本元素:
box.innerText = "<div>卡布奇诺今犹在,不见当年倒茶人</div>"

由于textContent不考虑实际渲染样式,所以在外面修改内容时浏览器不需要重新计算布局,在性能上相比innerText更加稳定,如果要频繁对页面内容进行修改,建议优先考虑textContent。
除了使用inner获取内容外,我们还可以使用outer来获取包含元素本身在内的全部内容:
console.log(box.outerHTML)

注意,当我们替换outerHTML内容时,会连带整个标签一起变化。
还有outerText,但是这个属性默认情况下和innerText其实是一致的,不过,如果我们尝试修改它,它会连带着整个标签一起变成普通文本:
box.outerText = "牛逼啊"

创建和操作元素
前面我们介绍了如何修改内容,只需要利用innerHTML / innerText / textContent属性即可。但是实际上,直接对innerHTML进行修改会导致一些性能问题,这一节我们就介绍一下更多关于 DOM 的增删改操作。
想在页面中新增一个元素,第一步一定是创建它,document对象为我们提供了一个createElement方法可以快速创建一个新的元素:
const div = document.createElement('div')
div.textContent = '我是新创建的元素' //和普通元素一样,可以正常设置属性
不过,这仅仅只是在内存中创建了一个新的元素,此时在页面上还看不到任何变化。创建完元素之后,一定要插入到 DOM 树中,我们可以使用appendChild方法来将它添加到某个元素的内部,作为子元素存在:
const box = document.getElementById('box')
const div = document.createElement('div')
div.textContent = '我是新创建的元素'
//调用父元素的appendChild方法为其添加这个新创建的元素到内部
box.appendChild(div)

注意,appendChild只会在原有基础上新增子元素,如果原本就存在子元素,则会自动添加到原本存在元素的后面:

还有一个比较有意思的是,如果这个元素不是我们创建的,而是原本在页面上就存在的,那么将其添加到新的位置后,相当于是从原来的位置移动到这个新的位置上,而不是复制一个新的元素插入:
const box = document.getElementById('box')
const item = document.querySelector('.item')
//实际上是将这个元素移动到box的下面
box.appendChild(item)
除了原本的appendChild,我们也可以使用append方法来实现元素插入,效果是完全一样的,但是它支持填入多个元素,按照顺序一次性插入:
const box = document.getElementById('box')
const div = document.createElement('div')
div.textContent = '我是新创建的元素'
const item = document.querySelector('.item')
box.append(div, item)
此外,除了插入元素之外,append还可以直接插入一个字符串,这个字符串直接作为文本元素存入。
const box = document.getElementById('box')
box.append("我是文本内容")

这里除了创建简单元素之外,实际上document还为我们提供了很多不同的节点类型创建:
document.createTextNode('hello') //可以实现创建一个文本节点
document.createComment("这是注释") //创建注释
这里需要注意的是,虽然append可以实现节点插入,但是依然会出现前面提到的性能问题,如果我们需要一次性插入多个节点,建议使用DocumentFragment,它就像是一个临时的小DOM树,也可以增删DOM元素,我们可以先把要插入的元素放入其中,之后一次性进行插入:
const fragment = document.createDocumentFragment(); //创建一个临时dom树
//添加元素到fragment中
box.append(fragment)
这也可以极大地优化批量插入元素的情况,相比for循环每次插入都会导致元素重新计算发生重排,这种方式会大大减少重排次数,提高性能。
当然,既然可以在尾部插入元素,我们也可以在首部插入元素,新元素会出现在 第一个子节点的位置:
const box = document.getElementById('box')
box.prepend(div, item, "我是文本内容") //效果和上面一样,但是在元素内部的前面插入
我们还可以实现精确位置插入,使用insertBefore可以实现在指定元素的前面插入,比如我们现在想要插入到这个p子标签的前面:
<div id="box">
<a>你干嘛</a>
<p>哎哟</p>
</div>
const box = document.getElementById('box')
const div = document.createElement('div')
div.textContent = '我是新创建的元素'
//除了直接使用document来查询元素之外,我们还可以对着任意一个元素进行查询
const p = box.querySelector('p')
box.insertBefore(div, p) //这里的意思将div插入到p之前
不过,这种方式只能往某个元素的前面插入,用起来不是很方便。这里更建议大家使用更加万能的元素插入操作insertAdjacentElement,它的参数可以自由控制插入点,相比前面几种更加方便:
const box = document.getElementById('box')
const div = document.createElement('div')
div.textContent = '我是新创建的元素'
const p = box.querySelector('p')
//第一个参数用于控制插入点,beforebegin 就是在它本身之前
box.insertAdjacentElement('beforebegin', div)

| position | 位置说明 | 示意 |
|---|---|---|
"beforebegin" |
在 target 前面(当兄弟) | target 之前 |
"afterbegin" |
在 target 内部最前 | 第一个子元素 |
"beforeend" |
在 target 内部最后 | 最后一个子元素 |
"afterend" |
在 target 后面(当兄弟) | target 之后 |
如果只是单纯插入一个文本元素,那么直接使用insertAdjacentText,它可以实现普通文本插入:
box.insertAdjacentText('beforebegin', "我是文本内容")
当然,如果你希望插入一段HTML代码,也可以使用insertAdjacentText:
box.insertAdjacentText('beforebegin', "我是文本内容")
除此之外,还有insertAdjacentHTML方法可以实现对HTML代码的插入,但是注意,这种方式和innerHTML一样,存在一些性能问题和XSS攻击问题。
介绍完元素的插入,我们接着来看元素的删除,删除非常简单,只需要直接对着元素自己调用remove方法即可:
const box = document.getElementById('box')
box.remove() //移除自己
当然,我们也可以通过removeChild来移除子元素,但是这里需要传入子元素(那我干嘛不直接对着子元素remove呢)
const box = document.getElementById('box')
const p = box.querySelector('p')
box.removeChild(p)
然后是元素的替换操作,使用replaceChild即可替换某个指定元素:
const box = document.getElementById('box')
const p = box.querySelector('p')
const a = document.createElement('a')
a.textContent = '点我啊哥哥'
//第一个参数是新的元素,第二个参数是原本需要被替换的
box.replaceChild(a, p)
我们还可以使用replaceChildren来实现对所有子元素的替换:
box.replaceChildren(a, "我是普通文本元素")
replaceChildren会移除所有子元素,并将给到的元素替换进去。
除了对子元素进行替换之外我们也可以直接对某个元素进行替换,使用replaceWith替换:
const box = document.getElementById('box')
const a = document.createElement('a')
a.textContent = '点我啊哥哥'
//直接替换调用元素
box.replaceWith(a)
//也可以一次性传入多个元素,包括文本元素也是可以的,注意不会进行HTML转换
box.replaceWith(a, "我是普通文本元素")