跟隨 Web 標準探究DOM -- Node 與 Element 的遍歷

寫在前面

這篇沒有什麼 WebKit 代碼的分析,由於……沒啥好分析的,在實現裏無非就是樹的(先序DFS)遍歷而已,囧哈哈哈……在WebCore/dom/Node.hWebCore/dom/ContainerNode.hWebCore/dom/Element.h 以及對應的 .cpp 裏看兩眼就好了。下面這些屬性通常都做爲了私有變量直接放在了對象裏(按照命名規範基本都叫m_xxx),而後經過和標準同名的 public 方法返回。不過要注意一下它們放在了哪裏,好比Node裏和子節點相關的方法通常定義到了 ContainerNode.h,Node 裏須要意識到 Element 存在的方法通常放去了 Element.h (即便定義時是Node::xxx 這樣的)。html

這篇主要分析一下對做爲 Node 的元素和做爲 Element 的元素進行遍歷的不一樣,以及總結一下各瀏覽器對這些 API 的兼容性。node

Node 的遍歷

Node 繼承 EventTargetDocumentDocumentFragmentElement繼承Node,因此下面提到的屬性DocumentDocumentFragmentElement均可以用。git


Node.parentNode

標準

DOM 1定義在 Node interface,原型readonly attribute Node parentNode,指明DocumentDocumentFragmentAttr 和不在樹中的 node 的 parentNodenullgithub

DOM 2DOM 3WHATWGDOM 4 都和 DOM 1 一致web

注意點

這是一個只讀屬性,因此不能通給一個元素的parentNode賦值來移動它,任何對這個引用的賦值操做都會被無視。好比:瀏覽器

node.parentNode = anotherNode;
console.log(node.parentNode === anotherNode); // false

可是你能夠修改它的parentNode的屬性。app

node.parentNode.title = "foo";
console.log(node.parentNode.title); // foo

此外,DocumentAttr 沒有parentNode還好理解,可是 Attr 沒有就有點很差理解了,並且EntityNotation也是沒有的 —— 反向理解,Node.childNodes也是不算 attribute node,entity node 之類的,人家不把你當孩子,你也不必把人家當父母。dom

沒有 parent 的 Node(好比剛剛用createElement建立或者用removeChild刪除)的這個屬性是 null。spa

兼容性

IE8- 裏的 parentNode 有幾個 bug:code

新建立的元素的 parentNode 是 null,但修改過內容(好比用innerHTML或者appendChild)以後就會變成 DocumentFragment

var foo = document.createElement('div');
console.log(foo.parentNode);  // null
foo.innerHTML = "bar"
console.log(foo.parentNode);  // [object HTMLDocument]
console.log(foo.parentNode.nodeType);  // 11 = DocumentFragment

從文檔中刪掉的節點,parentNodeDocumentFragment。對以下 HTML:

<div id="foo">
    <div id="bar"></div>
</div>

執行 JS:

var foo = document.getElementById('bar');
console.log(foo.parentNode);  // [object HTMLDivElement]
foo.parentNode.removeChild(foo);
console.log(foo.parentNode);  // [object HTMLDocument]
console.log(foo.parentNode.nodeType);  // 11 = DocumentFragment

Node.firstChildNode.lastChild

標準

DOM 1(firstChildlastChild)定義在 Node interface,原型readonly attribute Node firstChildreadonly attribute Node lastChild,指明DocumentDocumentFragmentAttr 和不在樹中的 node 的 parentNodenull

DOM 2(firstChildlastChild),DOM 3(firstChildlastChild), WHATWG (firstChildlastChild),DOM 4(firstChildlastChild) 和 DOM 1 一致

注意點

這是一個只讀屬性,和parentNode同樣是不能從新賦值的。

注意瀏覽器可能(並且不少都)將 text node 和 comment node 算在一個 node 的 child nodes 裏(HTML 文本里的縮進和斷行都會算成新的 text node 夾雜在元素之間),而且 document.firstNode 多是 doctype,所以不能斷定 firstChild 返回的是一個元素,若是想獲得第一個元素的話,須要手動檢查nodeType並日後過濾。

CSS pseudo element 不會被算入。

W3C FAQ 解釋了爲何有 DOM 的實現會將空白字符算做 text node:

DOM 必須將處理過的 XML (且爲了方便,不少 DOM 的實現會將 XML 與 HTML 的許多處理合並)原文所有交給應用,空白字符也不能丟掉(這樣 DOM 樹與 XML 文本才能完成一一映射),那麼就應該找個類型的 node 將它塞進去了 -- 最合適的就是 text node。

兼容性

IE 8- 不將空白的 text node 算做子節點,IE 9+及其餘瀏覽器都算。對以下HTML:

<div id="foo">

</div>

執行 JS:

var foo = document.getElementById('foo');
console.log(foo.firstChild);  // null in IE8-, supposed be a text node

Node.nextSiblingNode.previousSibling

標準

DOM 1(previousSiblingnextSibling)定義在 Node interface,原型readonly attribute Node previousSiblingreadonly attribute Node nextSibling,不存在對應 node 的返回 null

DOM 2(previousSiblingnextSibling),DOM 3(previousSiblingnextSibling)和 DOM 1 一致。

WHATWG (previousSiblingnextSibling) 和 W3C DOM 一致,另外說明了 sibling 的概念樹中相對位置的概念(按照tree order,即先序DFS)

DOM 4(previousSiblingnextSibling)和 WHATWG 一致。

注意點

Node.firstChildNode.lastChild的注意事項相似。

兼容性

IE 8- 不將空白的 text node 算做 sibling,IE 9+及其餘瀏覽器都算。

HTML:

<div></div>  <div id="foo"></div>

JS:

var foo = document.getElementById('foo');
// [object HTMLDivElement] in IE8-, supposed to be a text node
console.log(foo.previousSibling);

Node.childNodes

標準

DOM 1定義在 Node interface,原型readonly attribute NodeList childNodes,指明瞭返回的 NodeList 是 live 的,且若是沒有子節點時返回空的 NodeList.

DOM 2DOM 3 和 DOM 1 一致。

WHATWG 原型 [SameObject] readonly attribute NodeList childNodes,和 W3C DOM 一致。DOM 4 和 WHATWG 同樣。

注意點

Node.firstChildNode.lastChild的注意事項相似。返回的NodeList元素是隻讀的(能夠改元素屬性,不能夠改引用)。要增刪子元素的話對childNodes動腦筋是沒用的……(注意:其餘瀏覽器對childNodes中引用的修改僅僅是無視,但 IE 會怒報錯)

HTML:

<div id="foo"><p></p></div>

JS:

var foo = document.getElementById('foo');
console.log(foo.childNodes.length);  // 1

var bar = document.createElement('div');

foo.childNodes[0] = bar;  // attempt to replace a child, throws error in IE
console.log(foo.childNodes[0].nodeName);  // "P", not replaced

foo.childNodes[1] = bar;  // attempt to add a child, throws error in IE
console.log(foo.childNodes.length);  // 1, not added

delete foo.childNodes[0];  // attempt to delete a child, throws error in IE
console.log(foo.childNodes.length);  // 1, not deleted

通常document.childNodes 只有 doctype 和 <html>元素,除非原文二者之間有註釋。

元素的排列順序是 document order,即按照 DOM 樹中的先序 DFS 排列。

兼容性

IE 8- 不將空白的 text node 算做子節點,IE 9+及其餘瀏覽器都算。

HTML:

<div id="foo">   </div>

JS:

var foo = document.getElementById('foo');
// 0 in IE8-, supposed to be 1
console.log(foo.childNodes.length);

Element 的遍歷

ElementNode 的區別在於 Element 不包括 text node,comment node,etc. 實際上,Element 繼承自 Node,也就是說它原本就是 Node 的一種。Element 都具有(或者說,應該具有) Node.nodeType == Node.ELEMENT_NODE 這個特性(還有其餘哪幾種nodeType參閱WHATWG標準,這裏先不展開敘述)。如下的幾種 API 能夠當作 Node 版的 API 加上對結果進行Node.nodeType == Node.ELEMENT_NODE過濾(實際上 WebKit 的實現也基本都是這樣乾的)。

注意做爲 Element 的遍歷 API 基本都屬於 HTML5 的新特性,W3C 標準裏通常都只能在 DOM 4 裏找到。


Node.parentElement

標準

WHATWG 將 parentElement 定義在了 Node ,原型readonly attribute Element? parentElementW3C DOM 4 也同樣。

乍一看,定義在Node彷佛有點怪,不過仔細一想實際上是很合理的 —— Element 的子節點不必定是 Element,譬如 text node。你不能阻礙人家尋親的能力啊 :D

注意點

若是 Node 的父元素不是 Element,返回的是 null。

兼容性

實際上 parentElement 一開始是 IE 特有的(起碼從 IE6 開始就有了),但 IE 僅爲 Element 定義了這個屬性(便是說 text node 之類的是不能用的)。此後這個屬性進入了標準,目前基本各大瀏覽器都支持它,主要的兼容性問題出如今 IE 不支持非 ElementNode 使用這個屬性。若是僅對 Element 使用它的話,是能夠放心用的。

此外因爲 IE8- 中 parentNode 有不輕的 bug(見前文),在只須要 Element 的場景下,可能用 parentElement 是更好的選擇。


ParentNode.firstElementChildParentNode.lastElementChild

標準

目前 WHATWG 將 firstElementChildlastElementChild 定義在了 ParentNode,原型爲

readonly attribute Element? firstElementChild;
readonly attribute Element? lastElementChild;

它們本來在ElementTraversal,後來爲了下降耦合,WHATWG 將 ElementTraversal 按照功能分割成了兩個 interface ParentNodeChildNode,而 firstElementChildlastElementChild天然就挪去了針對有子元素的Node設置的ParentNode

目前繼承 ParentNode 的包括DocumentElementDocumentFragment,因此這三個 interface 的對象是能夠訪問firstElementChildlastElementChild的。

W3C DOM4 和 WHATWG 一致,可是注意 DOM4 目前還不是 W3C Recommendation。目前處於 W3C Recommendation 狀態的標準裏, firstElementChildlastElementChild仍然定義在 ElementTraversal。按照 Element Traversal 標準的規定,全部的 Element 都必須實現 ElementTraversal,但對其餘 interface 不做要求。

所以,這兩個屬性在 WHATWG 和 W3C 的標準裏存在分歧:WHATWG 標準中,DocumentElementDocumentFragment 均有這兩個屬性;W3C 標準中,目前僅有 Element 具備這兩個屬性。但由於和 WHATWG 一致的 DOM4 未來頗有可能成爲 W3C Recommendation,W3C 標準最後頗有可能會和 WHATWG 同樣,三種對象均有這兩個屬性。

注意點

若是沒有子元素,返回的是 null。這兩個屬性也是隻讀的,能夠在子元素上修改它的屬性,但不可更改引用(會被無視)。

兼容性

因爲屬於較新的 API,在Element上的使用要 IE 9+ 才支持,其餘瀏覽器的現行版本都有支持。

由於在 WHATWG 和 W3C 的現行標準裏存在分歧,DocumentDocumentFragment 對這兩個屬性的支持在各瀏覽器中不太一致。偏 WHATWG 的 Chrome,Firefox 和 Opera 支持 DocumentElementDocumentFragment,IE 9+ 和 Safari 僅支持 Element。考慮到 DOM4 未來應該會成爲 W3C Recommendation,最後應該是三個 interface 都能支持的(固然,IE 就不能期望舊版本支持了……)


NonDocumentTypeChildNode.nextElementSiblingNonDocumentTypeChildNode.previousElementSibling

標準

在 WHATWG 標準裏,和爲了照顧 jQuery 兼容性而爲getElementById 專門設一個 NonElementParentNode (而不是ParentNode)相似,爲了照顧現存網頁的兼容性,nextElementSiblingpreviousElementSibling 被定義在了一個專門分出來的 NonDocumentTypeChildNode(而不是ChildNode)裏,參見 bug tracker上的討論

目前 NonDocumentTypeChildNode 的定義以下:

[NoInterfaceObject]
interface NonDocumentTypeChildNode {
  readonly attribute Element? previousElementSibling;
  readonly attribute Element? nextElementSibling;
};
Element implements NonDocumentTypeChildNode;
CharacterData implements NonDocumentTypeChildNode;

注:目前 WHATWG 標準裏 ParentNodeNonElementParentNodeChildNodeNonDocumentTypeChildNode 之間的關係以下圖:

W3C DOM4 與 WHATWG 一致,但與ParentNode.firstElementChildParentNode.lastElementChild的狀況相似的是,按照目前處於 W3C Recommendation 的 Element Traversal 的定義,只有 Element 擁有這兩個屬性,CharacterData 沒有。

注意點

相似 ParentNode.firstElementChildParentNode.lastElementChild

兼容性

也與 ParentNode.firstElementChildParentNode.lastElementChild相似,須要 IE9+。Chrome,Firefox 和 Opera 支持 ElementCharacterData上訪問這兩個屬性,IE 9+ 和 Safari 僅支持 Element, 若是 W3C DOM 4 進入 Recommendation,極可能會統一。


ParentNode.childElementCount

標準

WHATWG / DOM4 定義在 ParentNode,原型readonly attribute unsigned long childElementCount。W3C Recommendation 裏目前定義在 ElementTraversal,原型和 WHATWG 同樣。

注意點

在符合標準的實現裏,約等於 container.children.length

兼容性

ParentNode.firstElementChild 的狀況相似,須要 IE9+,Chrome,Firefox 和 Opera 支持 DocumentElementDocumentFragment,IE 9+ 和 Safari 僅支持 Element


ParentNode.children

標準

雖然這個 API 很早就存在,但直到最近才標準化。WHATWG / DOM4 定義在ParentNode,原型[SameObject] readonly attribute HTMLCollection children,指明是一個 live 的 HTMLCollection 而不是NodeList,也就是說元素必然全是 Element(歷史遺留問題帶來的囧命名,和Node那邊的名字對不上號,不叫childElements而叫children,不叫ElementList而叫HTMLCollection……)。

注意點

相似 Node.childNodes,獲得的 HTMLCollection 是 live 且(引用)只讀的。

兼容性

該屬性最先出如今 IE 中,IE6 開始具有這個屬性。此後各大瀏覽器跟着實現,Firefox是最後一個實現這個屬性的主要瀏覽器(3.5開始,也蠻久了)。可是因爲 WHATWG 標準的接受度不一樣,Chrome,Firefox 和 Opera 在支持 DocumentElementDocumentFragment上使用該屬性,IE 和 Safari 僅支持 Element。 Chrome 和 Firefox 還實驗性地支持在 SVGElement 上使用該屬性。

另外,IE8- 的 children 會包含 comment node。

HTML:

<div id="foo"><!-- comment --></div>

JS:

var foo = document.getElementById('foo');
console.log(foo.children.length);  // 1, supposed to be 0
相關文章
相關標籤/搜索