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()。