DOM 的剖析
DOM 将 XML 或 HTML 文档表示为树。本页介绍了 DOM 树的基本结构以及用于导航它的各种属性和方法。
首先,我们需要介绍一些与树相关的概念。树是一种由节点组成的数据结构。每个节点都包含一些数据。节点以分层方式组织——除了根节点(它没有父节点)之外,每个节点都有一个父节点,以及一个有序列表,包含零个或多个子节点。现在我们可以定义以下内容:
- 没有父节点的节点称为树的根。
- 没有子节点的节点称为叶子。
- 共享相同父节点的节点称为兄弟节点。兄弟节点属于其父节点的同一子节点列表,因此它们具有明确的顺序。
- 如果我们通过重复跟随父链接可以从节点 A 到节点 B,则 A 是 B 的后代,B 是 A 的祖先。
- 树中的节点按树顺序排列,首先列出节点本身,然后按顺序递归列出其每个子节点(前序,深度优先遍历)。
以下是树的一些重要属性:
- 每个节点都与唯一的根节点相关联。
- 如果节点 A 是节点 B 的父节点,则节点 B 是节点 A 的子节点。
- 不允许循环:任何节点都不能是其自身的祖先或后代。
Node 接口及其子类
DOM 中的所有节点都由实现 Node 接口的对象表示。Node 接口体现了许多之前定义的概念:
parentNode属性返回父节点,如果节点没有父节点,则返回null。childNodes属性返回子节点的NodeList。firstChild和lastChild属性分别返回此列表的第一个和最后一个元素,如果没有子节点,则返回null。getRootNode()方法通过重复跟随父链接返回包含节点的树的根。hasChildNodes()方法如果它有任何子节点(即它不是叶子),则返回true。previousSibling和nextSibling属性分别返回上一个和下一个兄弟节点,如果没有这样的兄弟节点,则返回null。contains()方法如果给定节点是该节点的后代,则返回true。compareDocumentPosition()方法按树顺序比较两个节点。比较节点部分更详细地讨论了此方法。
您很少直接使用普通的 Node 对象——相反,DOM 中的所有对象都实现继承自 Node 的接口之一,这些接口表示文档中的附加语义。节点类型限制了它们包含的数据以及有效子节点类型。考虑以下 HTML 文档如何在 DOM 中表示:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<h1>Hello, world!</h1>
<p>This is a paragraph.</p>
</body>
</html>
它生成以下 DOM 树:
此 DOM 树的根是一个 Document 节点,它表示整个文档。此节点作为 document 变量全局公开。此节点有两个重要的子节点:
- 一个可选的
DocumentType节点,表示 doctype 声明。在我们的例子中,有一个。此节点也可以通过Document节点的doctype属性访问。 - 一个可选的
Element节点,表示根元素。对于 HTML 文档(例如我们的情况),这通常是HTMLHtmlElement。对于 SVG 文档,这通常是SVGSVGElement。此节点也可以通过Document节点的documentElement属性访问。
DocumentType 节点始终是叶节点。Element 节点是文档内容大部分的表示形式。它下面的每个元素,例如 <head>、<body> 和 <p>,也由 Element 节点表示。事实上,每个都是 Element 的子类,特定于该标签名称,定义在 HTML 规范中,例如 HTMLHeadElement 和 HTMLBodyElement,具有额外的属性和方法来表示该元素的语义,但这里我们重点关注 DOM 的共同行为。Element 节点可以有其他 Element 节点作为子节点,表示嵌套元素。例如,<head> 元素有三个子节点:两个 <meta> 元素和一个 <title> 元素。此外,元素还可以有 Text 节点和 CDATASection 节点作为子节点,表示文本内容。例如,<p> 元素有一个子节点,一个包含字符串“This is a paragraph.”的 Text 节点。Text 节点和 CDATASection 节点始终是叶节点。
所有可以有子节点的节点(Document、DocumentFragment 和 Element)都允许两种类型的子节点:Comment 和 ProcessingInstruction 节点。这些节点始终是叶节点。
除了子节点之外,每个元素还可以有属性,表示为 Attr 节点。Attr 扩展了 Node 接口,但它们不是主树结构的一部分,因为它们不是任何节点的子节点,并且它们的父节点是 null。相反,它们存储在一个单独的命名节点映射中,可以通过 Element 节点的 attributes 属性访问。
Node 接口定义了一个 nodeType 属性,指示节点的类型。总结一下,我们介绍了以下节点类型:
| 节点类型 | nodeType 值 |
有效子节点(除了 Comment 和 ProcessingInstruction) |
|---|---|---|
Document |
Node.DOCUMENT_NODE (9) |
DocumentType, Element |
DocumentType |
Node.DOCUMENT_TYPE_NODE (10) |
None |
Element |
Node.ELEMENT_NODE (1) |
Element, Text, CDATASection |
文本 |
Node.TEXT_NODE (3) |
None |
CDATASection |
Node.CDATA_SECTION_NODE (4) |
None |
Comment |
Node.COMMENT_NODE (8) |
None |
ProcessingInstruction |
Node.PROCESSING_INSTRUCTION_NODE (7) |
None |
Attr |
Node.ATTRIBUTE_NODE (2) |
None |
注意:您可能注意到我们在这里跳过了一些节点类型。Node.ENTITY_REFERENCE_NODE (5)、Node.ENTITY_NODE (6) 和 Node.NOTATION_NODE (12) 值不再使用,而 Node.DOCUMENT_FRAGMENT_NODE (11) 值将在构建和更新 DOM 树中介绍。
每个节点的数据
每种节点类型都有其自己的方式来表示其持有的数据。Node 接口本身定义了三个与数据相关的属性,总结在下表中:
| 节点类型 | nodeName |
nodeValue |
textContent |
|---|---|---|---|
Document |
"#document" |
null |
null |
DocumentType |
它的 name(例如 "html") |
null |
null |
Element |
它的 tagName(例如 "HTML"、"BODY") |
null |
按树顺序连接所有其文本节点后代 |
文本 |
"#text" |
它的 data |
它的 data |
CDATASection |
"#cdata-section" |
它的 data |
它的 data |
Comment |
"#comment" |
它的 data |
它的 data |
ProcessingInstruction |
它的 target |
它的 data |
它的 data |
Attr |
它的 name |
它的 value |
它的 value |
Document
Document 节点本身不持有任何数据,因此其 nodeValue 和 textContent 始终为 null。其 nodeName 始终为 "#document"。
Document 确实定义了一些关于文档的元数据,这些数据来自环境(例如,提供文档的 HTTP 响应):
URL和documentURI属性返回文档的 URL。characterSet属性返回文档使用的字符编码,例如"UTF-8"。compatMode属性返回文档的渲染模式,可以是"CSS1Compat"(标准模式)或"BackCompat"(怪异模式)。contentType属性返回文档的 媒体类型,例如 HTML 文档的"text/html"。
DocumentType
文档中的 DocumentType 如下所示:
<!doctype name PUBLIC "publicId" "systemId">
您可以指定三个部分,它们对应于 DocumentType 节点的三个属性:name、publicId 和 systemId。对于 HTML 文档,doctype 始终是 <!doctype html>,因此 name 是 "html",并且 publicId 和 systemId 都是空字符串。
Element
文档中的 Element 如下所示:
<p class="note" id="intro">This is a paragraph.</p>
除了内容之外,您还可以指定两个部分:标签名称和属性。标签名称对应于 Element 节点的 tagName 属性,在本例中为 "P"(请注意,对于 HTML 元素,它始终为大写)。属性对应于存储在 Element 节点的 attributes 属性中的 Attr 节点。我们将在元素及其属性部分更详细地讨论属性。
Element 节点本身不持有任何数据,因此其 nodeValue 始终为 null。其 textContent 是按树顺序连接所有其文本节点后代的结果,在本例中为 "This is a paragraph."。对于以下元素:
<div>Hello, <span>world</span>!</div>
textContent 是 "Hello, world!",它连接了文本节点 "Hello, "、<span> 元素内的文本节点 "world" 和文本节点 "!"。
CharacterData
Text、CDATASection、Comment 和 ProcessingInstruction 都继承自 CharacterData 接口,该接口是 Node 的子类。CharacterData 接口定义了一个属性 data,它保存节点的文本内容。data 属性也用于实现这些节点的 nodeValue 和 textContent 属性。
对于 Text 和 CDATASection,data 属性保存节点的文本内容。在以下文档中(请注意我们使用 SVG 文档,因为 HTML 不允许 CDATA 部分):
<text>Some text</text>
<style><![CDATA[h1 { color: red; }]]></style>
<text> 元素内的文本节点的 data 为 "Some text",<style> 元素内的 CDATA 节的 data 为 "h1 { color: red; }"。
对于 Comment,data 属性保存注释的内容,从 <!-- 之后开始,到 --> 之前结束。例如,在以下文档中:
<!-- This is a comment -->
注释节点的 data 为 " This is a comment "。
对于 ProcessingInstruction,data 属性保存处理指令的内容,从目标之后开始,到 ?> 之前结束。例如,在以下文档中:
<?xml-stylesheet type="text/xsl" href="style.xsl"?>
处理指令节点的 data 为 'type="text/xsl" href="style.xsl"',其 target 为 "xml-stylesheet"。
此外,CharacterData 接口定义了 length 属性,它返回 data 字符串的长度,以及 substringData() 方法,它返回 data 的子字符串。
Attr
对于以下元素:
<p class="note" id="intro">This is a paragraph.</p>
<p> 元素有两个属性,由两个 Attr 节点表示。每个属性都包含一个名称和一个值,对应于 name 和 value 属性。第一个属性的 name 为 "class",value 为 "note",而第二个属性的 name 为 "id",value 为 "intro"。
元素及其属性
如前所述,Element 节点的属性由 Attr 节点表示,这些节点存储在一个单独的命名节点映射中,可以通过 Element 节点的 attributes 属性访问。此 NamedNodeMap 接口定义了三个重要属性:
length,它返回属性的数量。item()方法,它返回给定索引处的Attr。getNamedItem()方法,它返回具有给定名称的Attr。
Element 接口还定义了几个直接操作属性的方法,而无需访问命名节点映射:
element.getAttribute(name)等价于element.attributes.getNamedItem(name).value(如果属性存在)。element.getAttributeNode(name)等价于element.attributes.getNamedItem(name)。element.hasAttribute(name)等价于element.attributes.getNamedItem(name) !== null。element.getAttributeNames()返回所有属性名称的数组。element.hasAttributes()等价于element.attributes.length > 0。
您还可以通过 Attr 节点的 ownerElement 属性访问属性的拥有元素。
有两个特殊属性 id 和 class,它们在 Element 接口上拥有自己的属性:id 和 className,它们反映了相应属性的值。此外,classList 属性返回一个 DOMTokenList,表示 class 属性中的类列表。
使用元素树
由于 Element 节点构成了文档结构的主干,您可以专门遍历元素节点,跳过其他节点(如 Text 和 Comment)。
- 对于所有节点,
parentElement属性如果父节点是Element,则返回父节点,如果父节点不是Element(例如,如果父节点是Document),则返回null。这与parentNode不同,后者无论父节点类型如何都返回父节点。 - 对于
Document、DocumentFragment和Element,children属性只返回子Element节点的HTMLCollection。这与childNodes不同,后者返回所有子节点。firstElementChild和lastElementChild属性分别返回此集合的第一个和最后一个元素,如果没有子元素,则返回null。childElementCount属性返回子元素的数量。 - 对于
Element和CharacterData,previousElementSibling和nextElementSibling属性返回上一个和下一个是Element的兄弟节点,如果没有这样的兄弟节点,则返回null。这与previousSibling和nextSibling不同,后者可能返回任何类型的兄弟节点。
比较节点
有三个重要的方法用于比较节点:isEqualNode()、isSameNode() 和 compareDocumentPosition()。
isSameNode() 方法是旧方法。现在,它的行为类似于 严格相等运算符 (===),当且仅当两个节点是同一个对象时才返回 true。
isEqualNode() 方法从结构上比较两个节点。如果两个节点具有相同的类型、相同的数据,并且它们的子节点在每个索引处也相等,则它们被认为是相等的。在每个节点的数据部分,我们已经定义了每种节点类型相关的数据:
- 对于
Document,没有数据,因此只需要比较子节点。 - 对于
DocumentType,需要比较name、publicId和systemId属性。 - 对于
Element,需要比较tagName(更准确地说,是namespaceURI、prefix和localName;我们将在XML 命名空间指南中介绍这些)和属性。 - 对于
Attr,需要比较name(更准确地说,是namespaceURI、prefix和localName;我们将在XML 命名空间指南中介绍这些)和value属性。 - 对于所有
CharacterData节点(Text、CDATASection、Comment和ProcessingInstruction),需要比较data属性。对于ProcessingInstruction,还需要比较target属性。
a.compareDocumentPosition(b) 方法按树顺序比较两个节点。它返回一个位掩码,指示它们的相对位置。可能的情况有:
- 如果
a和b是同一个节点,则返回0。 - 如果两个节点都是同一元素节点的属性,则如果
a在属性列表中位于b之前,则返回Node.DOCUMENT_POSITION_PRECEDING | Node.DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC(34),如果a在b之后,则返回Node.DOCUMENT_POSITION_FOLLOWING | Node.DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC(36)。如果任一节点是属性,则使用其所有者元素进行进一步比较。 - 如果两个节点没有相同的根节点,则返回
Node.DOCUMENT_POSITION_DISCONNECTED | Node.DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC | Node.DOCUMENT_POSITION_PRECEDING(35) 或Node.DOCUMENT_POSITION_DISCONNECTED | Node.DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC | Node.DOCUMENT_POSITION_FOLLOWING(37)。返回哪一个取决于实现。 - 如果
a是b的祖先(包括b是a的属性时),则返回Node.DOCUMENT_POSITION_CONTAINS | Node.DOCUMENT_POSITION_PRECEDING(10)。 - 如果
a是b的后代(包括a是b的属性时),则返回Node.DOCUMENT_POSITION_CONTAINED_BY | Node.DOCUMENT_POSITION_FOLLOWING(20)。 - 如果
a在树顺序中位于b之前,则返回Node.DOCUMENT_POSITION_PRECEDING(2)。 - 如果
a在树顺序中位于b之后,则返回Node.DOCUMENT_POSITION_FOLLOWING(4)。
使用位掩码值,因此您可以使用位与运算来检查特定关系。例如,要检查 a 是否位于 b 之前,您可以执行:
if (a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_PRECEDING) {
// a precedes b
}
这考虑了 a 和 b 是同一元素的属性,a 是 b 的祖先,以及 a 在树顺序中位于 b 之前的情况。
总结
以下是我们迄今为止介绍的所有功能。虽然很多,但它们在不同场景下都很有用。
- DOM 中的所有节点都实现
Node接口。 - 要导航 DOM 树:
parentNode、childNodes、firstChild/lastChild、hasChildNodes()、getRootNode()、previousSibling/nextSibling。 - 要导航元素树:
parentElement、children、firstElementChild/lastElementChild、childElementCount、previousElementSibling/nextElementSibling。 nodeType属性指示节点的类型。nodeName、nodeValue和textContent属性提供节点持有的数据。Document节点及其两个重要的子节点:doctype和documentElement。DocumentType节点及其三个属性:name、publicId和systemId。Element节点及其属性:tagName、attributes。Attr节点及其属性:name和value。CharacterData接口及其属性:data。- 四个
CharacterData子类:Text、CDATASection、Comment和ProcessingInstruction。ProcessingInstruction还具有target属性。 - 各种处理属性的方式,包括
id、className和classList属性。 - 比较节点的三个方法:
isEqualNode()、isSameNode()和compareDocumentPosition()。