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 之前的情況。

總結

以下是我們迄今為止介紹的所有功能。雖然很多,但它們在不同場景下都很有用。