寫在前面
這篇沒有什么 WebKit 代碼的分析,因為……沒啥好分析的,在實現里無非就是樹的(先序DFS)遍歷而已,囧哈哈哈……在WebCore/dom/Node.h , WebCore/dom/ContainerNode.h 和 WebCore/dom/Element.h 以及對應的 .cpp 里看兩眼就行了。下面這些屬性一般都作為了私有變量直接放在了對象里(按照命名規范基本都叫m_xxx),然后通過和標准同名的 public 方法返回。不過要注意一下它們放在了哪里,比如Node里和子節點相關的方法一般定義到了 ContainerNode.h,Node 里需要意識到 Element 存在的方法一般放去了 Element.h (即使定義時是Node::xxx 這樣的)。
這篇主要分析一下對作為 Node 的元素和作為 Element 的元素進行遍歷的不同,以及總結一下各瀏覽器對這些 API 的兼容性。
Node 的遍歷
Node 繼承 EventTarget,Document,DocumentFragment,Element繼承Node,所以下面提到的屬性Document,DocumentFragment,Element都可以用。
Node.parentNode
標准
DOM 1定義在 Node interface,原型readonly attribute Node parentNode,指明Document,DocumentFragment,Attr 和不在樹中的 node 的 parentNode 為 null。
DOM 2,DOM 3,WHATWG,DOM 4 都和 DOM 1 一致
注意點
這是一個只讀屬性,所以不能通給一個元素的parentNode賦值來移動它,任何對這個引用的賦值操作都會被無視。比如:
node.parentNode = anotherNode; console.log(node.parentNode === anotherNode); // false
但是你可以修改它的parentNode的屬性。
node.parentNode.title = "foo"; console.log(node.parentNode.title); // foo
此外,Document 和 Attr 沒有parentNode還好理解,但是 Attr 沒有就有點不好理解了,而且Entity 和Notation也是沒有的 —— 反向理解,Node.childNodes也是不算 attribute node,entity node 之類的,人家不把你當孩子,你也沒必要把人家當父母。
沒有 parent 的 Node(比如剛剛用createElement創建或者用removeChild刪除)的這個屬性是 null。
兼容性
IE8- 里的 parentNode 有幾個 bug:
新創建的元素的 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
從文檔中刪掉的節點,parentNode是DocumentFragment。對如下 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.firstChild 和 Node.lastChild
標准
DOM 1(firstChild,lastChild)定義在 Node interface,原型readonly attribute Node firstChild和readonly attribute Node lastChild,指明Document,DocumentFragment,Attr 和不在樹中的 node 的 parentNode 為 null。
DOM 2(firstChild,lastChild),DOM 3(firstChild,lastChild), WHATWG (firstChild,lastChild),DOM 4(firstChild,lastChild) 和 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.nextSibling 和 Node.previousSibling
標准
DOM 1(previousSibling,nextSibling)定義在 Node interface,原型readonly attribute Node previousSibling和readonly attribute Node nextSibling,不存在對應 node 的返回 null。
DOM 2(previousSibling,nextSibling),DOM 3(previousSibling,nextSibling)和 DOM 1 一致。
WHATWG (previousSibling,nextSibling) 和 W3C DOM 一致,另外說明了 sibling 的概念 和 樹中相對位置的概念(按照tree order,即先序DFS)
DOM 4(previousSibling,nextSibling)和 WHATWG 一致。
注意點
和Node.firstChild 與 Node.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.
WHATWG 原型 [SameObject] readonly attribute NodeList childNodes,和 W3C DOM 一致。DOM 4 和 WHATWG 一樣。
注意點
和Node.firstChild 與 Node.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 的遍歷
Element 與 Node 的區別在於 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? parentElement。W3C DOM 4 也一樣。
乍一看,定義在Node似乎有點怪,不過仔細一想其實是很合理的 —— Element 的子節點不一定是 Element,譬如 text node。你不能阻礙人家尋親的能力啊 :D
注意點
如果 Node 的父元素不是 Element,返回的是 null。
兼容性
實際上 parentElement 一開始是 IE 特有的(起碼從 IE6 開始就有了),但 IE 僅為 Element 定義了這個屬性(即是說 text node 之類的是不能用的)。此后這個屬性進入了標准,目前基本各大瀏覽器都支持它,主要的兼容性問題出現在 IE 不支持非 Element 的 Node 使用這個屬性。如果僅對 Element 使用它的話,是可以放心用的。
此外由於 IE8- 中 parentNode 有不輕的 bug(見前文),在只需要 Element 的場景下,可能用 parentElement 是更好的選擇。
ParentNode.firstElementChild 和 ParentNode.lastElementChild
標准
目前 WHATWG 將 firstElementChild和lastElementChild 定義在了 ParentNode,原型為
readonly attribute Element? firstElementChild; readonly attribute Element? lastElementChild;
它們原本在ElementTraversal,后來為了降低耦合,WHATWG 將 ElementTraversal 按照功能分割成了兩個 interface ParentNode,ChildNode,而 firstElementChild和lastElementChild自然就挪去了針對有子元素的Node設置的ParentNode。
目前繼承 ParentNode 的包括Document,Element 和 DocumentFragment,所以這三個 interface 的對象是可以訪問firstElementChild和lastElementChild的。
W3C DOM4 和 WHATWG 一致,但是注意 DOM4 目前還不是 W3C Recommendation。目前處於 W3C Recommendation 狀態的標准里, firstElementChild和lastElementChild仍然定義在 ElementTraversal。按照 Element Traversal 標准的規定,所有的 Element 都必須實現 ElementTraversal,但對其他 interface 不作要求。
因此,這兩個屬性在 WHATWG 和 W3C 的標准里存在分歧:WHATWG 標准中,Document,Element 和 DocumentFragment 均有這兩個屬性;W3C 標准中,目前僅有 Element 具有這兩個屬性。但因為和 WHATWG 一致的 DOM4 將來很有可能成為 W3C Recommendation,W3C 標准最后很有可能會和 WHATWG 一樣,三種對象均有這兩個屬性。
注意點
如果沒有子元素,返回的是 null。這兩個屬性也是只讀的,可以在子元素上修改它的屬性,但不可更改引用(會被無視)。
兼容性
由於屬於較新的 API,在Element上的使用要 IE 9+ 才支持,其他瀏覽器的現行版本都有支持。
因為在 WHATWG 和 W3C 的現行標准里存在分歧,Document 和 DocumentFragment 對這兩個屬性的支持在各瀏覽器中不太一致。偏 WHATWG 的 Chrome,Firefox 和 Opera 支持 Document,Element 和 DocumentFragment,IE 9+ 和 Safari 僅支持 Element。考慮到 DOM4 將來應該會成為 W3C Recommendation,最后應該是三個 interface 都能支持的(當然,IE 就不能指望舊版本支持了……)
NonDocumentTypeChildNode.nextElementSibling 和 NonDocumentTypeChildNode.previousElementSibling
標准
在 WHATWG 標准里,和為了照顧 jQuery 兼容性而為getElementById 專門設一個 NonElementParentNode (而不是ParentNode)類似,為了照顧現存網頁的兼容性,nextElementSibling 和 previousElementSibling 被定義在了一個專門分出來的 NonDocumentTypeChildNode(而不是ChildNode)里,參見 bug tracker上的討論。
目前 NonDocumentTypeChildNode 的定義如下:
[NoInterfaceObject] interface NonDocumentTypeChildNode { readonly attribute Element? previousElementSibling; readonly attribute Element? nextElementSibling; }; Element implements NonDocumentTypeChildNode; CharacterData implements NonDocumentTypeChildNode;
注:目前 WHATWG 標准里 ParentNode,NonElementParentNode,ChildNode 和 NonDocumentTypeChildNode 之間的關系如下圖:

W3C DOM4 與 WHATWG 一致,但與ParentNode.firstElementChild 和 ParentNode.lastElementChild的情況類似的是,按照目前處於 W3C Recommendation 的 Element Traversal 的定義,只有 Element 擁有這兩個屬性,CharacterData 沒有。
注意點
類似 ParentNode.firstElementChild 和 ParentNode.lastElementChild。
兼容性
也與 ParentNode.firstElementChild 和 ParentNode.lastElementChild類似,需要 IE9+。Chrome,Firefox 和 Opera 支持 Element 和 CharacterData上訪問這兩個屬性,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 支持 Document,Element 和 DocumentFragment,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 在支持 Document,Element 和 DocumentFragment上使用該屬性,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
