這篇沒有什麼 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
這樣的)。html
這篇主要分析一下對做爲 Node 的元素和做爲 Element 的元素進行遍歷的不一樣,以及總結一下各瀏覽器對這些 API 的兼容性。node
Node
的遍歷Node
繼承 EventTarget
,Document
,DocumentFragment
,Element
繼承Node
,因此下面提到的屬性Document
,DocumentFragment
,Element
均可以用。git
Node.parentNode
DOM 1定義在 Node
interface,原型readonly attribute Node parentNode
,指明Document
,DocumentFragment
,Attr
和不在樹中的 node 的 parentNode
爲 null
。github
DOM 2,DOM 3,WHATWG,DOM 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
此外,Document
和 Attr
沒有parentNode
還好理解,可是 Attr
沒有就有點很差理解了,並且Entity
和Notation
也是沒有的 —— 反向理解,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
從文檔中刪掉的節點,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