在浏览器环境中,每一个 HTML 文档都会被解析为一棵由节点构成的树状结构。JavaScript 提供了丰富的接口来访问这棵树,但很多人在写代码时并没有意识到自己操作的是“节点树”还是“元素树”,从而在处理列表、表格等内容时出现意料之外的空值或换行符。理解这两类树的区别,是写好 DOM 遍历逻辑的前提。

一、节点树与元素树的根本区别
节点树(Node Tree)是文档最完整的表达形式,它的每一个单位都是 Node 类型的实例。除了我们熟悉的标签节点(元素节点,nodeType 为 1),节点树还包含文本节点(nodeType 为 3)、注释节点(nodeType 为 8)、文档节点(nodeType 为 9)等。当你通过 childNodes 属性获取子节点时,拿到的是节点树视角下的全部直接子级。
元素树(Element Tree)则可以理解为节点树的一个“子集投影”,它只保留元素节点,忽略文本与注释。通过 children 属性获取的就是元素树下的子元素集合,类型为 HTMLCollection。在绝大多数业务开发中,我们关心的只是标签之间的嵌套与内容,因此元素树更符合直觉,也能避免空格与换行被当作文本节点处理。
下面用一段简单的 HTML 来说明两者的差异:
<ul id="list"> <li>第一项</li> <li>第二项</li> </ul>
如果在这个结构上分别读取 childNodes 和 children,会得到不同长度的集合。前者包含三个子节点:两个 li 元素之间还有一个由换行和缩进组成的文本节点;后者只包含两个 li 元素。
二、基础遍历属性与方法
在节点树中,常用的导航属性包括 parentNode、childNodes、firstChild、lastChild、nextSibling 和 previousSibling。这些属性会严格按照节点树返回结果,文本与注释节点也在其列。例如使用 firstChild 时,如果标签开头有空格,拿到的可能是文本节点而非元素。
对应地,元素树提供了一套更“干净”的属性:parentElement、children、firstElementChild、lastElementChild、nextElementSibling 和 previousElementSibling。它们只跨越元素节点,在编写组件或提取数据时更安全。下面的代码展示了如何用元素树属性遍历一个容器中的所有直接子标签:
const container = document.getElementById('list');
let el = container.firstElementChild;
while (el) {
console.log(el.tagName, el.textContent);
el = el.nextElementSibling;
}
这种写法不会受到空白文本节点的干扰。如果你的运行环境较老,不支持 firstElementChild 等属性,也可以通过判断 nodeType === 1 来手动过滤 childNodes 中的非元素节点,但代码会更冗长。
三、递归与深度优先遍历
当结构嵌套较深时,仅靠兄弟节点导航不够用,通常需要递归访问整棵子树。对于节点树递归,要小心文本节点没有 childNodes 之外的子结构;对于元素树递归,则直接遍历 children 即可。下面给出一个通用的元素树深度优先遍历函数:
function walkElements(root, callback) {
// root 必须是元素节点
callback(root);
const kids = root.children;
for (let i = 0; i < kids.length; i++) {
walkElements(kids[i], callback);
}
}
// 使用示例:打印所有元素的标签名
walkElements(document.body, function(node) {
console.log(node.tagName);
});
这种递归方式逻辑清晰,适合需要对每个元素做统一处理的场景,例如收集所有带特定属性的输入框。不过当 DOM 规模非常大时,递归可能带来调用栈压力,此时可以改为基于栈或队列的迭代写法,将待处理元素存入数组依次弹出,避免深层嵌套导致的栈溢出。
如果只关心节点树,也可以写出类似的递归,但要先过滤节点类型。例如在抽取网页纯文本时,遇到文本节点就追加内容,遇到元素节点就继续向下,这样既能保留换行结构,又不会误把注释输出到结果里。
四、使用 TreeWalker 与 querySelectorAll
浏览器原生提供了 TreeWalker 接口,用于在节点树中按条件高效遍历。通过 document.createTreeWalker 可以指定根节点、遍历的节点类型以及过滤函数。它比手动递归更节省内存,也更容易控制只访问元素或只访问文本。
// 仅遍历元素节点
const walker = document.createTreeWalker(
document.body,
NodeFilter.SHOW_ELEMENT,
null
);
let current = walker.nextNode();
while (current) {
console.log(current.tagName);
current = walker.nextNode();
}
与之相比,querySelectorAll 虽然不属于“一步步走”的遍历,但它在底层同样基于文档树搜索,能以 CSS 选择器一次性拿到元素树中的匹配集合。对于已知结构的查询,如获取所有 input 或某个 class 下的 li,它更简洁且可读性高。不过它返回的是静态 NodeList,不会随 DOM 变动自动更新,这一点与 children 这类实时集合不同。
| 方式 | 所属树 | 实时性 | 适用场景 |
|---|---|---|---|
| children / firstElementChild | 元素树 | 实时 | 简单层级导航 |
| childNodes / firstChild | 节点树 | 实时 | 需处理文本或注释 |
| TreeWalker | 节点树(可筛选) | 实时 | 大型结构按类型遍历 |
| querySelectorAll | 元素树 | 静态 | 选择器批量获取 |
五、实际开发中的选择建议
在普通业务里,如果只想操作标签,优先使用元素树相关属性,能少写很多过滤逻辑。当需要从用户输入的富文本中抽取纯文字,或分析包含注释的模板时,再回到节点树,用 nodeType 做判断。对于动态列表渲染后的遍历,结合 children 与索引访问通常比递归更直接。
需要补充的是,现代框架大多屏蔽了手动遍历,但在写底层工具、浏览器插件或爬虫脚本时,DOM 遍历依旧是核心能力。明确节点树与元素树的边界,不仅能减少 bug,还能让代码意图更清晰,便于后续维护。
JavaScriptDOM_traversalnode_tree修改时间:2026-08-08 02:54:37