DOM 的剖析

DOM 将 XML 或 HTML 文档表示为树。本页介绍了 DOM 树的基本结构以及用于导航它的各种属性和方法。

首先,我们需要介绍一些与树相关的概念。树是一种由节点组成的数据结构。每个节点都包含一些数据。节点以分层方式组织——除了根节点(它没有父节点)之外,每个节点都有一个父节点,以及一个有序列表,包含零个或多个子节点。现在我们可以定义以下内容:

  • 没有父节点的节点称为树的
  • 没有子节点的节点称为叶子
  • 共享相同父节点的节点称为兄弟节点。兄弟节点属于其父节点的同一子节点列表,因此它们具有明确的顺序。
  • 如果我们通过重复跟随父链接可以从节点 A 到节点 B,则 A 是 B 的后代,B 是 A 的祖先
  • 树中的节点按树顺序排列,首先列出节点本身,然后按顺序递归列出其每个子节点(前序,深度优先遍历)。

以下是树的一些重要属性:

  • 每个节点都与唯一的根节点相关联。
  • 如果节点 A 是节点 B 的父节点,则节点 B 是节点 A 的子节点。
  • 不允许循环:任何节点都不能是其自身的祖先或后代。

Node 接口及其子类

DOM 中的所有节点都由实现 Node 接口的对象表示。Node 接口体现了许多之前定义的概念:

  • parentNode 属性返回父节点,如果节点没有父节点,则返回 null
  • childNodes 属性返回子节点的 NodeListfirstChildlastChild 属性分别返回此列表的第一个和最后一个元素,如果没有子节点,则返回 null
  • getRootNode() 方法通过重复跟随父链接返回包含节点的树的根。
  • hasChildNodes() 方法如果它有任何子节点(即它不是叶子),则返回 true
  • previousSiblingnextSibling 属性分别返回上一个和下一个兄弟节点,如果没有这样的兄弟节点,则返回 null
  • contains() 方法如果给定节点是该节点的后代,则返回 true
  • compareDocumentPosition() 方法按树顺序比较两个节点。比较节点部分更详细地讨论了此方法。

您很少直接使用普通的 Node 对象——相反,DOM 中的所有对象都实现继承自 Node 的接口之一,这些接口表示文档中的附加语义。节点类型限制了它们包含的数据以及有效子节点类型。考虑以下 HTML 文档如何在 DOM 中表示:

html
<!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 树:

The DOM tree of the previous HTML document

此 DOM 树的根是一个 Document 节点,它表示整个文档。此节点作为 document 变量全局公开。此节点有两个重要的子节点:

  • 一个可选的 DocumentType 节点,表示 doctype 声明。在我们的例子中,有一个。此节点也可以通过 Document 节点的 doctype 属性访问。
  • 一个可选的 Element 节点,表示根元素。对于 HTML 文档(例如我们的情况),这通常是 HTMLHtmlElement。对于 SVG 文档,这通常是 SVGSVGElement。此节点也可以通过 Document 节点的 documentElement 属性访问。

DocumentType 节点始终是叶节点。Element 节点是文档内容大部分的表示形式。它下面的每个元素,例如 <head><body><p>,也由 Element 节点表示。事实上,每个都是 Element 的子类,特定于该标签名称,定义在 HTML 规范中,例如 HTMLHeadElementHTMLBodyElement,具有额外的属性和方法来表示该元素的语义,但这里我们重点关注 DOM 的共同行为。Element 节点可以有其他 Element 节点作为子节点,表示嵌套元素。例如,<head> 元素有三个子节点:两个 <meta> 元素和一个 <title> 元素。此外,元素还可以有 Text 节点和 CDATASection 节点作为子节点,表示文本内容。例如,<p> 元素有一个子节点,一个包含字符串“This is a paragraph.”的 Text 节点。Text 节点和 CDATASection 节点始终是叶节点。

所有可以有子节点的节点(DocumentDocumentFragmentElement)都允许两种类型的子节点:CommentProcessingInstruction 节点。这些节点始终是叶节点。

除了子节点之外,每个元素还可以有属性,表示为 Attr 节点。Attr 扩展了 Node 接口,但它们不是主树结构的一部分,因为它们不是任何节点的子节点,并且它们的父节点是 null。相反,它们存储在一个单独的命名节点映射中,可以通过 Element 节点的 attributes 属性访问。

Node 接口定义了一个 nodeType 属性,指示节点的类型。总结一下,我们介绍了以下节点类型:

节点类型 nodeType 有效子节点(除了 CommentProcessingInstruction
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 节点本身不持有任何数据,因此其 nodeValuetextContent 始终为 null。其 nodeName 始终为 "#document"

Document 确实定义了一些关于文档的元数据,这些数据来自环境(例如,提供文档的 HTTP 响应):

  • URLdocumentURI 属性返回文档的 URL。
  • characterSet 属性返回文档使用的字符编码,例如 "UTF-8"
  • compatMode 属性返回文档的渲染模式,可以是 "CSS1Compat"(标准模式)或 "BackCompat"(怪异模式)。
  • contentType 属性返回文档的 媒体类型,例如 HTML 文档的 "text/html"

DocumentType

文档中的 DocumentType 如下所示:

xml
<!doctype name PUBLIC "publicId" "systemId">

您可以指定三个部分,它们对应于 DocumentType 节点的三个属性:namepublicIdsystemId。对于 HTML 文档,doctype 始终是 <!doctype html>,因此 name"html",并且 publicIdsystemId 都是空字符串。

Element

文档中的 Element 如下所示:

html
<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."。对于以下元素:

html
<div>Hello, <span>world</span>!</div>

textContent"Hello, world!",它连接了文本节点 "Hello, "<span> 元素内的文本节点 "world" 和文本节点 "!"

CharacterData

TextCDATASectionCommentProcessingInstruction 都继承自 CharacterData 接口,该接口是 Node 的子类。CharacterData 接口定义了一个属性 data,它保存节点的文本内容。data 属性也用于实现这些节点的 nodeValuetextContent 属性。

对于 TextCDATASectiondata 属性保存节点的文本内容。在以下文档中(请注意我们使用 SVG 文档,因为 HTML 不允许 CDATA 部分):

svg
<text>Some text</text>
<style><![CDATA[h1 { color: red; }]]></style>

<text> 元素内的文本节点的 data"Some text"<style> 元素内的 CDATA 节的 data"h1 { color: red; }"

对于 Commentdata 属性保存注释的内容,从 <!-- 之后开始,到 --> 之前结束。例如,在以下文档中:

html
<!-- This is a comment -->

注释节点的 data" This is a comment "

对于 ProcessingInstructiondata 属性保存处理指令的内容,从目标之后开始,到 ?> 之前结束。例如,在以下文档中:

xml
<?xml-stylesheet type="text/xsl" href="style.xsl"?>

处理指令节点的 data'type="text/xsl" href="style.xsl"',其 target"xml-stylesheet"

此外,CharacterData 接口定义了 length 属性,它返回 data 字符串的长度,以及 substringData() 方法,它返回 data 的子字符串。

Attr

对于以下元素:

html
<p class="note" id="intro">This is a paragraph.</p>

<p> 元素有两个属性,由两个 Attr 节点表示。每个属性都包含一个名称和一个值,对应于 namevalue 属性。第一个属性的 name"class"value"note",而第二个属性的 name"id"value"intro"

元素及其属性

如前所述,Element 节点的属性由 Attr 节点表示,这些节点存储在一个单独的命名节点映射中,可以通过 Element 节点的 attributes 属性访问。此 NamedNodeMap 接口定义了三个重要属性:

  • length,它返回属性的数量。
  • item() 方法,它返回给定索引处的 Attr
  • getNamedItem() 方法,它返回具有给定名称的 Attr

Element 接口还定义了几个直接操作属性的方法,而无需访问命名节点映射:

您还可以通过 Attr 节点的 ownerElement 属性访问属性的拥有元素。

有两个特殊属性 idclass,它们在 Element 接口上拥有自己的属性:idclassName,它们反映了相应属性的值。此外,classList 属性返回一个 DOMTokenList,表示 class 属性中的类列表。

使用元素树

由于 Element 节点构成了文档结构的主干,您可以专门遍历元素节点,跳过其他节点(如 TextComment)。

  • 对于所有节点,parentElement 属性如果父节点是 Element,则返回父节点,如果父节点不是 Element(例如,如果父节点是 Document),则返回 null。这与 parentNode 不同,后者无论父节点类型如何都返回父节点。
  • 对于 DocumentDocumentFragmentElementchildren 属性只返回子 Element 节点的 HTMLCollection。这与 childNodes 不同,后者返回所有子节点。firstElementChildlastElementChild 属性分别返回此集合的第一个和最后一个元素,如果没有子元素,则返回 nullchildElementCount 属性返回子元素的数量。
  • 对于 ElementCharacterDatapreviousElementSiblingnextElementSibling 属性返回上一个和下一个是 Element 的兄弟节点,如果没有这样的兄弟节点,则返回 null。这与 previousSiblingnextSibling 不同,后者可能返回任何类型的兄弟节点。

比较节点

有三个重要的方法用于比较节点:isEqualNode()isSameNode()compareDocumentPosition()

isSameNode() 方法是旧方法。现在,它的行为类似于 严格相等运算符 (===),当且仅当两个节点是同一个对象时才返回 true

isEqualNode() 方法从结构上比较两个节点。如果两个节点具有相同的类型、相同的数据,并且它们的子节点在每个索引处也相等,则它们被认为是相等的。在每个节点的数据部分,我们已经定义了每种节点类型相关的​​数据:

  • 对于 Document,没有数据,因此只需要比较子节点。
  • 对于 DocumentType,需要比较 namepublicIdsystemId 属性。
  • 对于 Element,需要比较 tagName(更准确地说,是 namespaceURIprefixlocalName;我们将在XML 命名空间指南中介绍这些)和属性。
  • 对于 Attr,需要比较 name(更准确地说,是 namespaceURIprefixlocalName;我们将在XML 命名空间指南中介绍这些)和 value 属性。
  • 对于所有 CharacterData 节点(TextCDATASectionCommentProcessingInstruction),需要比较 data 属性。对于 ProcessingInstruction,还需要比较 target 属性。

a.compareDocumentPosition(b) 方法按树顺序比较两个节点。它返回一个位掩码,指示它们的相对位置。可能的情况有:

  • 如果 ab 是同一个节点,则返回 0
  • 如果两个节点都是同一元素节点的属性,则如果 a 在属性列表中位于 b 之前,则返回 Node.DOCUMENT_POSITION_PRECEDING | Node.DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC (34),如果 ab 之后,则返回 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)。返回哪一个取决于实现。
  • 如果 ab 的祖先(包括 ba 的属性时),则返回 Node.DOCUMENT_POSITION_CONTAINS | Node.DOCUMENT_POSITION_PRECEDING (10)。
  • 如果 ab 的后代(包括 ab 的属性时),则返回 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 之前,您可以执行:

js
if (a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_PRECEDING) {
  // a precedes b
}

这考虑了 ab 是同一元素的属性,ab 的祖先,以及 a 在树顺序中位于 b 之前的情况。

总结

以下是我们迄今为止介绍的所有功能。虽然很多,但它们在不同场景下都很有用。