当浏览器加载一个 HTML 页面时,它会将标记文本解析成一棵由各种节点构成的树状结构,这棵树就是 DOM(文档对象模型)。理解 DOM 节点树的结构和节点类型,是操作页面内容、处理交互的基础。所有后续的节点查找、增删改操作,都是在这棵树上进行的。
13.1.1 什么是 DOM 节点树
HTML 文档中的每一个组成部分——标签、文本、注释、属性——都是树上的一个 节点(Node) 。这些节点按照文档的嵌套关系构成父子层级,形成一棵以 document 为根的树。比如下面这段简单的 HTML:
<!DOCTYPE html>
<html>
<head>
<title>示例页面</title>
</head>
<body>
<h1>标题</h1>
<p>这是一段文字。</p>
</body>
</html>
浏览器会将它解析成如下结构的节点树(简化版):
document
└── html
├── head
│ └── title
│ └── "示例页面" (文本节点)
└── body
├── h1
│ └── "标题"
└── p
└── "这是一段文字。"
在这个结构中,html 是 head 和 body 的父节点;h1 和 p 是兄弟节点;title 下面的文本“示例页面”是一个独立的文本节点,而不是标签的一部分。这种层级关系就是程序操作页面的唯一入口。
13.1.2 核心节点类型
所有的节点都实现了 Node 接口,并根据具体类型派生出不同的子接口。开发者最常接触的节点类型有以下几种:
1. 元素节点(Element Node)
- 对应 HTML 标签,如
<div>、<p>、<span>。 - 节点类型值:
1(Node.ELEMENT_NODE)。 - 提供了访问属性、子节点、样式等丰富的方法(通过
HTMLElement等子接口)。
const h1 = document.querySelector('h1');
console.log(h1.nodeType); // 1
console.log(h1.nodeName); // "H1"(大写)
2. 文本节点(Text Node)
- 元素中的纯文本内容,例如
<p>Hello</p>中的 “Hello”。 - 节点类型值:
3(Node.TEXT_NODE)。 - 文本节点是元素节点的子节点,可以通过
nodeValue或data获取和修改文本。
const text = h1.firstChild;
console.log(text.nodeType); // 3
console.log(text.nodeValue); // "标题"
3. 属性节点(Attr Node)
- 表示标签上的属性,如
id="main"、class="active"。 - 节点类型值:
2(Node.ATTRIBUTE_NODE),但在 DOM4 之后属性节点不再独立存在于树中,而是作为元素节点的属性集合存在。 - 在实际开发中,一般通过
element.getAttribute()、element.setAttribute()或element.id、element.className属性来操作,很少直接创建Attr对象。
4. 注释节点(Comment Node)
- HTML 中的注释
<!-- 注释内容 -->。 - 节点类型值:
8(Node.COMMENT_NODE)。 - 在遍历节点时有时会意外遇到注释,需要注意过滤。
5. 文档节点(Document Node)
- 整个 DOM 树的根节点,即
document对象。 - 节点类型值:
9(Node.DOCUMENT_NODE)。 document是访问所有节点的入口,提供了getElementById、querySelector等查找方法。
6. 文档片段节点(DocumentFragment Node)
- 一个轻量级的临时容器,用于批量操作节点。
- 节点类型值:
11(Node.DOCUMENT_FRAGMENT_NODE)。 - 当向 fragment 中添加子节点时,不会引发页面回流(reflow),最后一次性插入真实 DOM 树,可显著提升性能。常用在批量插入大量元素时。
13.1.3 节点之间的关系属性
在节点树中,节点之间有几种核心关系,JavaScript 提供了对应的属性来访问:
// 以 childNode 为例
const p = document.querySelector('p');
// 父节点
p.parentNode; // 获取父节点(任何类型)
p.parentElement; // 获取父元素节点(为 null 如果父节点不是元素)
// 子节点
p.childNodes; // 所有子节点(包含文本、注释)— NodeList
p.children; // 只有子元素节点 — HTMLCollection
p.firstChild; // 第一个子节点(可能是文本)
p.lastChild; // 最后一个子节点
p.firstElementChild; // 第一个子元素节点
p.lastElementChild; // 最后一个子元素节点
// 兄弟节点
p.previousSibling; // 前一个兄弟节点(可能是文本)
p.nextSibling; // 后一个兄弟节点
p.previousElementSibling; // 前一个兄弟元素节点
p.nextElementSibling; // 后一个兄弟元素节点
重要区别:childNodes 包含所有节点类型(文本、注释等),而 children 仅包含元素节点。大多数情况下,我们操作的是元素节点,所以 children、firstElementChild 等方法更常用。但需要注意,换行和缩进也会生成文本节点,这是新手常遇到的坑。
13.1.4 判断节点类型与节点信息
node.nodeType:返回节点类型对应的整数(1 元素,3 文本,8 注释等)。node.nodeName:返回节点名称,对元素来说就是大写的标签名,对文本节点是"#text",注释是"#comment"。node.nodeValue:对于文本和注释节点,返回或设置文本内容;对于元素节点,返回null。
const div = document.createElement('div');
div.innerHTML = 'Hello <!-- 注释 -->';
// div.childNodes:
// 0: 文本节点 "Hello "
// 1: 注释节点
for (let node of div.childNodes) {
if (node.nodeType === Node.TEXT_NODE) {
console.log('文本:', node.nodeValue);
} else if (node.nodeType === Node.COMMENT_NODE) {
console.log('注释:', node.nodeValue);
}
}
13.1.5 节点树的动态性
DOM 节点树是 实时 的。当使用 querySelectorAll 返回的静态 NodeList 时(确切说是方法返回时才抓取快照),但 getElementsByClassName、childNodes 等返回的是 动态 的集合。一旦文档结构发生变化,这些集合会自动反映改变。例如:
const list = document.getElementById('list');
const items = list.getElementsByTagName('li'); // 动态 HTMLCollection
console.log(items.length); // 3
const newLi = document.createElement('li');
list.appendChild(newLi);
console.log(items.length); // 4,自动更新
这种动态特性在遍历节点并同时修改结构时容易引发意外,需要注意循环控制和性能影响。
13.1.6 理解节点树的价值
把文档看作节点树,是我们用程序操控页面的根基。一切 UI 交互——无论是获取用户输入、修改样式、响应用户点击——本质上都是在遍历和修改这棵树的节点。掌握了节点类型和节点关系,就具备了程序化控制页面的基础能力。下一节将详细讲解如何有效地查找、创建和修改这些节点。