news 2026/5/8 17:13:14

跟着 MDN 学 HTML day_26:(DOM 的树形结构与节点导航)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
跟着 MDN 学 HTML day_26:(DOM 的树形结构与节点导航)

引言

文档对象模型(DOM)是 Web 开发中最基础也最重要的概念之一。它将 XML 或 HTML 文档表示为一棵树形结构,为开发者提供了操作网页内容的标准化接口。理解 DOM 的树形解剖结构,掌握各种节点类型以及如何在树中穿行,是成为合格前端开发者的必经之路。本文将系统地介绍 DOM 树的基本结构、节点接口及其子类、各类节点的数据存储方式,以及在实际开发中常用的节点比较方法。

一、树形结构的基本概念

在深入 DOM 之前,我们需要先理解树这种数据结构的几个核心概念。一棵树由多个节点组成,节点之间形成严格的层级关系:每个节点有且只有一个父节点(根节点除外),并且可以拥有零个或多个子节点。

下面这个简单的对象结构可以直观地展示树的基本概念:

// 用对象模拟一棵简单的树结构consttree={data:"根节点",// 根节点:没有父节点的节点children:[{data:"叶子节点A",// 叶子节点:没有子节点的节点children:[]},{data:"分支节点",// 拥有子节点的中间节点children:[{data:"叶子节点B",// 与叶子节点C是兄弟节点(siblings)children:[]},{data:"叶子节点C",children:[]}]}]};// 验证几个树形结构的规则// 规则1:每个节点关联唯一的根节点// 规则2:如果A是B的父节点,则B是A的子节点// 规则3:树中不允许存在循环引用

树的遍历遵循前序深度优先原则,即先访问节点本身,然后按照顺序递归访问其每一个子节点。DOM 树中的节点顺序正是按照这种方式排列的。

二、Node 接口及其核心属性

DOM 中的所有节点都是实现了 Node 接口的对象。Node 接口封装了树形结构的通用操作,使得我们能够在文档树中进行导航。以下代码展示了 Node 接口中最常用的导航属性和方法:

<divid="parent"><pid="first">第一段文字</p><pid="second">第二段文字</p><pid="third">第三段文字</p></div><script>constparent=document.getElementById('parent');constfirst=document.getElementById('first');constsecond=document.getElementById('second');constthird=document.getElementById('third');// parentNode: 获取父节点console.log(first.parentNode===parent);// true// childNodes: 获取所有子节点(包括文本节点)console.log(parent.childNodes.length);// 可能是7,因为标签之间的空白也会生成文本节点// firstChild 和 lastChild: 获取第一个和最后一个子节点console.log(parent.firstChild);console.log(parent.lastChild);// hasChildNodes(): 判断是否有子节点console.log(parent.hasChildNodes());// trueconsole.log(first.hasChildNodes());// true(文本节点是子节点)// getRootNode(): 获取根节点console.log(first.getRootNode()===document);// true// previousSibling 和 nextSibling: 获取相邻的兄弟节点console.log(second.previousSibling);console.log(second.nextSibling);// contains(): 判断一个节点是否是另一个节点的后代console.log(parent.contains(second));// trueconsole.log(first.contains(parent));// false</script>

需要注意的是,childNodes 返回的是 NodeList 对象,其中不仅包含元素节点,还包含文本节点。HTML 源代码中的换行和缩进都会在 DOM 树中产生文本节点,这一点在实际开发中经常容易被忽视。

三、DOM 树的节点类型详解

一个完整的 HTML 文档在 DOM 中由多种不同类型的节点共同组成。以下是一个典型的 HTML 文档及其在 DOM 中的表示:

<!DOCTYPEhtml><htmllang="zh-CN"><head><metacharset="utf-8"/><title>DOM 树示例</title></head><body><h1>你好,世界!</h1><p>这是一个段落。</p></body></html>
// 查看 DOM 树的根节点——Document 节点console.log(document.nodeType);// 9 (Node.DOCUMENT_NODE)console.log(document.nodeName);// "#document"// doctype: DocumentType 节点,代表文档类型声明console.log(document.doctype);console.log(document.doctype.nodeType);// 10 (Node.DOCUMENT_TYPE_NODE)console.log(document.doctype.name);// "html"// DocumentType 始终是叶子节点,没有子节点// documentElement: 根元素节点,对于 HTML 文档通常是 <html> 元素console.log(document.documentElement);console.log(document.documentElement.nodeType);// 1 (Node.ELEMENT_NODE)console.log(document.documentElement.tagName);// "HTML"

在这个结构中,Document 节点处于最顶层,代表整个文档。它有两个重要的子节点:DocumentType 节点(如果声明了 doctype)和根元素节点。DocumentType 永远是叶子节点,而 Element 节点则承载了文档的绝大部分内容。

四、各类节点的数据存储方式

不同类型的节点以不同的方式存储自身的数据。Node 接口定义了 nodeName、nodeValue 和 textContent 三个通用属性,但它们在不同节点类型上的表现各不相同。

Document 节点与 DocumentType 节点

// Document 节点的数据属性console.log(document.nodeName);// "#document"console.log(document.nodeValue);// nullconsole.log(document.textContent);// null// Document 节点携带的文档元数据console.log(document.URL);// 当前文档的完整 URLconsole.log(document.documentURI);// 与 URL 相同console.log(document.characterSet);// 字符编码,如 "UTF-8"console.log(document.compatMode);// "CSS1Compat"(标准模式)或 "BackCompat"(怪异模式)console.log(document.contentType);// "text/html"// DocumentType 节点的数据属性constdoctype=document.doctype;console.log(doctype.name);// "html"console.log(doctype.publicId);// ""(HTML 文档通常为空字符串)console.log(doctype.systemId);// ""(HTML 文档通常为空字符串)

Element 节点

Element 节点本身不存储文本数据,它的 nodeValue 始终为 null。textContent 属性会返回其所有文本后代节点在树顺序下串联起来的字符串:

<divid="container">你好,<span>世界</span></div><script>constcontainer=document.getElementById('container');console.log(container.nodeValue);// nullconsole.log(container.textContent);// "你好,世界!" —— 所有文本节点的串联console.log(container.tagName);// "DIV"(HTML 元素始终大写)</script>

CharacterData 及其子类

Text、Comment、CDATASection 和 ProcessingInstruction 都继承自 CharacterData 接口,该接口的核心是 data 属性。以下示例展示了 Comment 和 Text 节点的数据:

<!-- 这是一条注释 --><div>这是一段文本内容</div><script>// 遍历 body 的子节点,识别不同的 CharacterData 类型constbodyChildren=document.body.childNodes;bodyChildren.forEach(node=>{if(node.nodeType===Node.COMMENT_NODE){console.log('找到注释节点:',node.data);// " 这是一条注释 "console.log('注释长度:',node.length);}if(node.nodeType===Node.TEXT_NODE){console.log('找到文本节点:',node.data);}});// 获取元素内的文本节点constdiv=document.querySelector('div');consttextNode=div.firstChild;console.log(textNode.nodeName);// "#text"console.log(textNode.nodeValue);// "这是一段文本内容"console.log(textNode.data);// "这是一段文本内容"console.log(textNode.length);// 9// substringData 方法console.log(textNode.substringData(0,2));// "这是"</script>

Text 节点和 CDATASection 节点始终是叶子节点。Comment 节点虽然在树的遍历中存在,但它始终是叶子节点,不能包含子节点。

五、元素及其属性的操作

Element 节点的属性由 Attr 节点表示,这些节点存储在一个独立的命名节点映射(NamedNodeMap)中,并非直接作为元素的子节点存在。Attr 节点的 parentNode 始终为 null。

以下代码展示了多种操作元素属性的方式:

<pclass="note highlight"id="intro"data-category="frontend">这是一段带有属性的段落。</p><script>constparagraph=document.querySelector('p');// 通过 attributes 属性访问 Attr 节点集合console.log(paragraph.attributes.length);// 3console.log(paragraph.attributes.item(0).name);// "class"console.log(paragraph.attributes.item(0).value);// "note highlight"// getNamedItem 方法constidAttr=paragraph.attributes.getNamedItem('id');console.log(idAttr.value);// "intro"console.log(idAttr.ownerElement===paragraph);// true// Element 接口提供的便捷方法console.log(paragraph.getAttribute('class'));// "note highlight"console.log(paragraph.getAttributeNode('id').value);// "intro"console.log(paragraph.hasAttribute('data-category'));// trueconsole.log(paragraph.getAttributeNames());// ["class", "id", "data-category"]console.log(paragraph.hasAttributes());// true// 特殊属性 id 和 class 的快捷访问console.log(paragraph.id);// "intro"console.log(paragraph.className);// "note highlight"// classList 属性返回 DOMTokenList 对象console.log(paragraph.classList);// DOMTokenListconsole.log(paragraph.classList.contains('note'));// trueconsole.log(paragraph.classList.length);// 2</script>

通过 getAttribute 直接获取属性值是最常见的做法。当需要更精细地操作 class 属性时,classList 提供的 add、remove、toggle 等方法比操作 className 字符串更加强大和安全。

六、在元素树中高效导航

由于 Element 节点构成了文档结构的主干,DOM 提供了一套专门用于在元素树中导航的属性,可以跳过文本节点和注释节点:

<divid="grandparent"><!-- 这条注释会被元素导航属性跳过 --><sectionid="parent-a"><p>AAA</p><p>BBB</p></section>一些游离的文本<sectionid="parent-b"><span>CCC</span></section></div><script>constgrandparent=document.getElementById('grandparent');constparentA=document.getElementById('parent-a');constparentB=document.getElementById('parent-b');// parentElement: 获取父元素节点(跳过非元素父节点)console.log(parentA.parentElement===grandparent);// true// children: 只包含子元素节点,忽略文本和注释console.log(grandparent.children.length);// 2console.log(grandparent.childNodes.length);// 可能为 5 或更多// firstElementChild 和 lastElementChildconsole.log(grandparent.firstElementChild===parentA);// trueconsole.log(grandparent.lastElementChild===parentB);// true// childElementCount: 子元素的数量console.log(grandparent.childElementCount);// 2// previousElementSibling 和 nextElementSiblingconsole.log(parentA.nextElementSibling===parentB);// trueconsole.log(parentB.previousElementSibling===parentA);// true// 对比:包含所有节点类型的 sibling 属性// nextSibling 可能会返回文本节点console.log(parentA.nextSibling);</script>

在实际开发中,优先使用带 Element 字样的导航属性可以避免因空白文本节点而导致的意外行为,代码的意图也更加清晰。

七、节点比较的三大利器

DOM 提供了三个用于比较节点的重要方法:isSameNode()、isEqualNode() 和 compareDocumentPosition()。它们分别在不同的层面上判断节点之间的关系。

isSameNode 与 isEqualNode

<divid="box-a"><p>内容一</p></div><divid="box-b"><p>内容一</p></div><script>constboxA=document.getElementById('box-a');constboxB=document.getElementById('box-b');// isSameNode(): 判断是否为同一个对象(现已不推荐,直接用 === )console.log(boxA.isSameNode(boxA));// trueconsole.log(boxA.isSameNode(boxB));// falseconsole.log(boxA===boxA);// true(等效写法)// isEqualNode(): 结构性相等判断// 需要类型相同、数据相同、子节点递归相等console.log(boxA.isEqualNode(boxB));// true(结构完全一致)console.log(boxA.isEqualNode(boxA));// true// 修改属性后会打破相等性boxB.setAttribute('data-x','1');console.log(boxA.isEqualNode(boxB));// false</script>

isEqualNode 执行的是深度比较,会递归检查所有子节点。对于比较大的 DOM 子树,这个操作可能会有一定的性能开销。

compareDocumentPosition 位掩码比较

compareDocumentPosition 方法返回一个位掩码,用于精确判断两个节点在树中的相对位置:

<divid="outer"><divid="inner"><spanid="deep">深层节点</span></div><divid="sibling"></div></div><script>constouter=document.getElementById('outer');constinner=document.getElementById('inner');constdeep=document.getElementById('deep');constsibling=document.getElementById('sibling');// a 是 b 的祖先letresult=outer.compareDocumentPosition(deep);console.log(result);// 10: Node.DOCUMENT_POSITION_CONTAINS (8) + Node.DOCUMENT_POSITION_PRECEDING (2)console.log(!!(result&Node.DOCUMENT_POSITION_CONTAINS));// true// a 是 b 的后代result=deep.compareDocumentPosition(outer);console.log(result);// 20: Node.DOCUMENT_POSITION_CONTAINED_BY (16) + Node.DOCUMENT_POSITION_FOLLOWING (4)console.log(!!(result&Node.DOCUMENT_POSITION_CONTAINED_BY));// true// a 在 b 之前(树顺序)result=inner.compareDocumentPosition(sibling);console.log(result);// 4: Node.DOCUMENT_POSITION_FOLLOWINGconsole.log(!!(result&Node.DOCUMENT_POSITION_FOLLOWING));// true// 实用封装:判断 a 是否位于 b 之前functionisBefore(a,b){return!!(a.compareDocumentPosition(b)&Node.DOCUMENT_POSITION_FOLLOWING);}console.log(isBefore(inner,sibling));// trueconsole.log(isBefore(sibling,inner));// false</script>

位掩码的优势在于可以同时表示多种关系。通过使用按位与运算符,我们能够简洁地检测特定的位置关系,这是在使用 compareDocumentPosition 时的标准做法。

八、总结

DOM 树是 Web 开发中最基础的数据结构,深入理解它的解剖结构能够让我们在处理复杂交互时更加游刃有余。本文涵盖的核心知识点包括:

在树形结构中,每个节点最多有一个父节点,根节点是唯一没有父节点的节点,叶子节点则没有子节点。DOM 中所有节点都实现了 Node 接口,通过 parentNode、childNodes、firstChild、lastChild 等属性可以在树中进行基本导航。完整文档由 Document 节点作为根,包含可选的 DocumentType 节点和必须的根元素节点。不同类型的节点(Element、Text、Comment、Attr 等)以各自的方式存储数据,nodeName、nodeValue 和 textContent 的表现各不相同。Attr 节点独立于主树存在,存储在 NamedNodeMap 中,Element 接口提供了 getAttribute、setAttribute 等便捷方法操作属性。针对元素树的导航属性(children、parentElement、nextElementSibling 等)可以跳过文本和注释节点,让元素间的遍历更加直接。节点比较方面,isEqualNode 用于深度结构相等判断,而 compareDocumentPosition 则通过位掩码精确描述两个节点在树中的位置关系。

系统性地掌握这些概念,将为后续学习 DOM 的动态构建、事件处理和性能优化打下坚实的基础。


想要解锁更多HTML 核心标签实战、前端零基础入门干货、开发避坑全指南吗?
持续关注,后续将更新CSS 布局实战、JavaScript 交互基础、全站导航开发等硬核内容,带你从新手快速进阶,轻松搞定前端开发!

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/8 17:12:31

从零开始理解 JDK 动态代理:保姆级教程

从零开始理解 JDK 动态代理&#xff1a;保姆级教程 1. 生活中的“代理”——先理解代理模式 你有没有找过明星合影&#xff1f;通常你没法直接联系到明星本人&#xff0c;而是通过经纪人。你把自己的请求&#xff08;合影&#xff09;告诉经纪人&#xff0c;经纪人判断是否能打…

作者头像 李华
网站建设 2026/5/8 17:11:56

工业物联网软件集成实战:DDS与OPC UA融合架构解析

1. 工业物联网系统集成的核心挑战与演进 在工业自动化领域摸爬滚打了十几年&#xff0c;我亲眼见证了系统集成重心的巨大转变。早期&#xff0c;我们这些工程师的精力几乎全耗在硬件上&#xff1a;如何把不同厂商的PLC&#xff08;可编程逻辑控制器&#xff09;通过Profibus或M…

作者头像 李华
网站建设 2026/5/8 17:11:40

如何将 4K 视频从 iPhone 传输到 PC/ Mac ?

4K 视频是一种高清视频格式&#xff0c;分辨率为 3840 x 2160 像素&#xff0c;是标准高清视频的四倍。虽然 4K 视频能够提供更细腻的图像和逼真的色彩&#xff0c;但它也占用更多存储空间。如果您用 iPhone 拍摄了太多 4K 视频&#xff0c;可能需要将 4K 视频从 iPhone 传输到…

作者头像 李华
网站建设 2026/5/8 17:11:30

FModel:如何用3个步骤轻松掌握虚幻引擎游戏资源提取技巧

FModel&#xff1a;如何用3个步骤轻松掌握虚幻引擎游戏资源提取技巧 【免费下载链接】FModel Unreal Engine Archives Explorer 项目地址: https://gitcode.com/gh_mirrors/fm/FModel 想要深入了解你喜爱的虚幻引擎游戏内部构造吗&#xff1f;FModel作为一款专业的虚幻引…

作者头像 李华
网站建设 2026/5/8 17:09:47

创业公司如何通过Taotoken低成本接入多模型支持产品迭代

&#x1f680; 告别海外账号与网络限制&#xff01;稳定直连全球优质大模型&#xff0c;限时半价接入中。 &#x1f449; 点击领取海量免费额度 创业公司如何通过Taotoken低成本接入多模型支持产品迭代 对于资源有限的创业公司而言&#xff0c;在产品中集成智能对话功能是一个…

作者头像 李华
网站建设 2026/5/8 17:09:11

从跑分到体验:DPD多尔蒂架构下射频功放器件选型与设计实战

1. 从“跑分”到“体验”&#xff1a;现代基站功放设计的范式转移十年前&#xff0c;如果有人问我&#xff0c;选一颗射频功率LDMOS管&#xff0c;最看重什么参数&#xff1f;我会毫不犹豫地回答&#xff1a;效率、增益、带宽。这就像在车展上挑跑车&#xff0c;谁的马力大、零…

作者头像 李华