1. 引言
DOM(Document Object Model,文档对象模型)是浏览器将 HTML 文档解析后生成的一棵节点树,它把页面中的每一个元素、文本、属性都抽象为节点,并通过树形结构组织起来。理解 DOM 节点树,是掌握 JavaScript 操作页面、实现动态交互的基础。本文将从节点树的构成、节点类型、遍历方式到实战案例,带你系统掌握 DOM 节点树的核心知识。
2. 什么是 DOM 节点树
当浏览器加载一个 HTML 页面时,会按照文档的层级结构,把每个标签、文本、注释等解析为一个节点,并按照父子、兄弟关系组织成一棵倒置的树。树的顶端是document节点,往下依次是html、head、body等元素节点。
以下面这段 HTML 为例:
<!DOCTYPE html> <html> <head> <title>示例页面</title> </head> <body> <h1>欢迎访问</h1> <p>这是一段<strong>加粗</strong>的文本</p> </body> </html>它对应的节点树结构如下:
flowchart TD A[document] --> B[html] B --> C[head] B --> D[body] C --> E[title] D --> F[h1] D --> G[p] G --> H[文本: 这是一段] G --> I[strong] G --> J[文本: 的文本] I --> K[文本: 加粗]可以看到,每个节点都有明确的父子关系,html是head和body的父节点,而head和body互为兄弟节点。
3. 节点类型
DOM 规范定义了多种节点类型,日常开发中最常用的是元素节点、文本节点和注释节点。我们可以通过nodeType属性来判断节点的类型。
| 节点类型 | nodeType 值 | 说明 |
|---|---|---|
| 元素节点 | 1 | 对应 HTML 标签,如div、p |
| 属性节点 | 2 | 标签上的属性,如id、class |
| 文本节点 | 3 | 标签内的文本内容 |
| 注释节点 | 8 | HTML 注释<!-- --> |
| 文档节点 | 9 | 整个文档的根节点document |
下面通过一段代码来验证节点类型:
// 获取 body 下的第一个子节点 const body = document.body; const firstChild = body.firstChild; console.log(body.nodeType); // 1,元素节点 console.log(firstChild.nodeType); // 可能是 3(文本节点)或 1(元素节点) // 遍历 body 的所有子节点并输出类型 for (let i = 0; i < body.childNodes.length; i++) { const node = body.childNodes[i]; console.log(`节点名称: ${node.nodeName}, 类型: ${node.nodeType}`); }需要注意的是,childNodes会包含文本节点(包括空白和换行),而children只包含元素节点,这是两者最直观的区别。
4. 节点树的核心属性
要熟练操作节点树,需要掌握节点之间的导航属性。这些属性帮助我们从一个节点出发,找到它的父节点、子节点和兄弟节点。
4.1 父子关系
// 获取父节点 const parent = element.parentNode; // 获取子节点列表(包含文本节点) const allChildren = element.childNodes; // 获取子元素列表(只包含元素节点) const elementChildren = element.children; // 获取第一个和最后一个子节点 const first = element.firstChild; const last = element.lastChild;4.2 兄弟关系
// 获取上一个和下一个兄弟节点 const prev = element.previousSibling; const next = element.nextSibling; // 只获取元素类型的兄弟节点 const prevElement = element.previousElementSibling; const nextElement = element.nextElementSibling;这里有一个常见的坑:previousSibling和nextSibling会把换行和空格当作文本节点返回,而previousElementSibling和nextElementSibling则只返回元素节点,推荐优先使用后者。
5. 遍历节点树
在实际开发中,我们经常需要遍历整棵节点树。除了使用递归,现代浏览器还提供了NodeIterator和TreeWalker两种遍历器。
5.1 使用 TreeWalker
// 创建一个 TreeWalker,只遍历元素节点 const walker = document.createTreeWalker( document.body, NodeFilter.SHOW_ELEMENT ); let node = walker.nextNode(); while (node) { console.log(node.tagName); node = walker.nextNode(); }5.2 使用 NodeIterator
// 创建 NodeIterator,遍历所有节点 const iterator = document.createNodeIterator( document.body, NodeFilter.SHOW_TEXT ); let textNode = iterator.nextNode(); while (textNode) { console.log(`文本内容: ${textNode.textContent}`); textNode = iterator.nextNode(); }相比手动递归,这两种方式更安全,因为它们会正确处理节点的新增和删除,不会出现死循环。
6. 实战:动态构建节点树
理解了节点树的结构后,我们通过一个完整的案例来巩固知识。下面的代码演示了如何动态创建一棵节点树,并把它插入到页面中。
// 创建一个卡片容器 const card = document.createElement('div'); card.className = 'card'; // 创建标题 const title = document.createElement('h2'); title.textContent = '动态节点树'; // 创建内容段落 const content = document.createElement('p'); content.textContent = '这段内容是通过 JavaScript 动态创建的。'; // 创建列表 const list = document.createElement('ul'); const items = ['节点创建', '节点插入', '节点删除']; items.forEach(text => { const li = document.createElement('li'); li.textContent = text; list.appendChild(li); }); // 组装节点树 card.appendChild(title); card.appendChild(content); card.appendChild(list); // 插入到页面中 document.body.appendChild(card);运行这段代码后,页面上会出现一张卡片,包含标题、段落和列表。整个过程展示了createElement、appendChild等核心 API 的用法。
7. 实战:节点树的删除与替换
除了创建节点,删除和替换也是高频操作。下面演示如何安全地移除和替换节点。
// 获取要删除的节点 const oldNode = document.getElementById('old-content'); // 方式一:通过父节点删除 oldNode.parentNode.removeChild(oldNode); // 方式二:直接删除(现代浏览器支持) oldNode.remove(); // 替换节点 const newParagraph = document.createElement('p'); newParagraph.textContent = '这是替换后的内容'; oldNode.parentNode.replaceChild(newParagraph, oldNode);需要注意的是,removeChild需要先获取父节点,而remove()方法更简洁,但兼容性稍差。在替换节点时,replaceChild的第一个参数是新节点,第二个参数是被替换的旧节点,顺序不要写反。
8. 总结
DOM 节点树是前端开发的核心概念之一。本文从节点树的定义出发,介绍了节点类型、导航属性、遍历方式,并通过实战案例演示了节点的创建、插入、删除和替换。掌握这些知识后,你就能更加灵活地操作页面结构,实现复杂的动态交互效果。建议读者在浏览器控制台中亲自运行本文的代码示例,加深对节点树的理解。