寫在前面
這篇沒有什么 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