遍歷DOM元素的children屬性遇到的坑

問題的引出

關於DOM元素的children屬性,之前我只在乎它和childNodes屬性的區別:即children屬性只會返回子元素節點集合,而childNodes返回的就不止元素節點,還有文本節點等全部子節點集合。這樣看來,children彷佛是咱們獲取子元素而捨棄其餘類型的子節點的最佳選擇,雖說在IE8-的瀏覽器下用它還會返回註釋節點,但兼容起來也是很簡單的。javascript

咱們知道,children返回的子元素集合其實是一個相似數組的HTMLCollection對象,那接下來咱們要獲取每一個子元素天然要遍歷它咯,可是一遍歷,問題就出來了:html

<div id="ul">
        <div id='i'></div>
        <div id="ii"></div>
        <div></div>
    </div>

    <script>
        o=document.getElementById('ul').children;
        for (i in o) {
            console.log(o[i]);
        }
    </script>

上面的代碼使用了for-in進行遍歷,但咱們預料中的結果並未出現,以chrome爲例,運行結果是這個:java

85ad0d9cjw1f1d5s4skb0j207y04cjrm.jpg

這就是我在前面的原生js練習題-第一課那篇文章中提到過的坑了,其中有兩個奇怪的問題:chrome

  1. 多返回了length等幾個在數組應該是不可枚舉的屬性。數組

  2. 把有id的元素重複了兩次。瀏覽器

關於問題1

先說一下,上面提到的第一點在各個瀏覽器裏狀況都相同,而第二點關於返回的元素是否重複在各瀏覽器下狀況還不一樣。spa

咱們先討論第一點,這裏要考慮for-in循環遍歷對象時的規則比較奇葩:對象自身和繼承到的可枚舉屬性都會被遍歷到。因此爲肯定多遍歷到的內容究竟是自身仍是原型上的屬性,咱們來驗證一下:code

console.log(Object.keys(o)); //["0","1","2","i","ii"]
    console.log(Object.getOwnPropertyNames(o)); //["0","1","2","i","ii"]

Object.keys()方法返回的是可枚舉的自身屬性的屬性名組成的數組,而Object.getOwnPropertyNames()返回的是全部自身屬性的屬性名組成的數組(含可枚舉和不可枚舉)。在這裏咱們沒有看到lengthitem()namedItem()三個屬性的身影,由此判定他們不是HTMLCollection對象自身的屬性,但既然能被for-in遍歷到那就只能是來自HTMLCollection原型的可枚舉屬性。咱們能夠用Object.getOwnPropertyDescriptor()來驗證其在原型上的可枚舉性:htm

console.log(Object.getOwnPropertyDescriptor(o.__proto__, 'length').enumerable); //true
    console.log(Object.getOwnPropertyDescriptor(o.__proto__, 'item').enumerable); //true
    console.log(Object.getOwnPropertyDescriptor(o.__proto__, 'namedItem').enumerable); //true

關於問題2

解決了多出來的三個屬性的來源,咱們再回過頭看看爲何會把有id的元素重複了兩次。觀察用Object.keys()方法返回的數組,這兩次一次用下標作屬性名、一次用id名做屬性名。但其實兩個屬性名指向的是同一個對象:對象

o[0]===o['i'] //true
    o[1]===o['ii'] //true

可見之因此for-in會把id的元素重複遍歷兩次,不是由於有id的元素都添加進HTMLCollection對象兩次,只是一個元素有了兩個屬性名而已,這是chrome的狀況(個人版本是48.0.2564.116 m),但放到火狐和IE下結果卻還有點所不一樣:

//FF
    console.log(Object.keys(o)); //["0", "1", "2"]
    console.log(Object.getOwnPropertyNames(o)); // ["0", "1", "2", "i", "ii"]
    o[0]===o['i'] //true
    o[1]===o['ii'] //true

    //IE11
    console.log(Object.keys(o)); //["i", "ii", "2"]
    console.log(Object.getOwnPropertyNames(o)); // ["i", "ii", "2"]
    o[0]===o['i'] //true
    o[1]===o['ii'] //true

可見雖然不一樣的瀏覽器返回的HTMLCollection對象都存在有id的子元素有兩個屬性名的狀況,但從Object.keys(o)的結果看,火狐和IE對同一元素默認只取一個屬性名。因此若是你在火狐或IE運行一開始那段代碼,就會發現for-in遍歷時火狐和IE也只對同一元素訪問一次,不會像chrome那樣重複遍歷。然而咱們還看到,這兩個瀏覽器間選取的屬性名也不一樣,火狐優先選擇下標形式,而IE優先選擇id形式,同時由Object.getOwnPropertyNames(o)的結果咱們還能夠窺探出火狐實現選取屬性名的機制多是經過將id形式的屬性名設爲不可枚舉來實現的,至於IE就不清楚了。

小結

這下咱們能夠得出結論了:children個屬性返回的HTMLCollection對象不止能遍歷到子元素,還能遍歷到來自其原型的lengthitem()namedItem()三個屬性。並且一旦遍歷到的子元素有id,就存在HTMLCollection對象裏一個元素會有兩個屬性名的問題,更讓人蛋疼的是各瀏覽器對這兩個屬性名的選取各不相同。固然,最根本緣由仍是由於children屬性如今還沒被正式歸入標準,在使用這種非標準屬性時咱們不免遇到一些奇葩的情況。

因此這也告誡咱們,若是對一個非標準屬性的特色不是特別瞭解,仍是不要輕易使用它,不然出現的問題每每是你難以控制的。但若是你仍是以爲children使用起來方便,那在使用時就得謹記這些問題,好比在遍歷子元素時最好放棄for-in循環,老老實實使用基本的for循環去遍歷數字索引吧,這樣就和遍歷數組差很少,不會遍歷到那些多出來的屬性了。

而至於for-in,最好只用來遍歷數組或簡單的對象。既要防止那些添加、修改了原型屬性的對象遍歷出多餘的的結果,也要防止相似children這種非標準屬性返回一個屬性的枚舉性不可控的對象的坑。

相關文章
相關標籤/搜索