文章出處

一、前言                                  

  大家先看看下面的js,猜猜結果會怎樣吧!

  可選答案:

  ①. 獲取id屬性值為id的節點元素

  ②. 拋namedItem is undefined的異常

var nodes = document.getElementsByName('dummyName');
var node = nodes.namedItem('id');

  答案是兩種都有可能哦!document.getElementsByName在Chrome和FF30.0中返回NodeList(木有namedItem方法的),在IE全系列中都返回HTMLCollection,吐血了吧?

  DOM集合又何止這些呢,下面我們就一起來探討一下吧!

 

二、困擾你我的NodeList與HTMLCollection              

  相同點:

    1. 類數組。有length屬性,可以用下標索引來訪問其中的元素,但沒有Array的slice等方法;

    2. 只讀。無法增刪其中的元素;

    3. 實時同步DOM樹的變化。若DOM樹有新元素加入,該類型的對象也會將新元素包含進來;

    4. 可通過下標數字類型索引獲取集合中指定位置的元素;

    5. 可通過item({String | Number} 索引)方法獲取集合中指定位置的元素,若通過索引找不到元素,則以第一個元素作為返回值。

  不同點(主要表現在HTMLCollection比NodeList能力更強大):

    1. HTMLCollection對象可通過namedItem({String} id或name)獲取首個匹配的元素,若沒有則返回null;

    2. HTMLCollection對象可通過點方式獲取第個id或name匹配的元素,若沒有則返回undefined。

  各瀏覽器選擇器返回類型差別:

// IE678 返回具有HTMLCollection特征(有namedItem方法)的[object Object]對象
// IE9、10、11、FF、Chrome均返回HTMLCollection
document.images; document.links; document.anchors; document.forms; document.embeds; document.scripts; document.applets; document.plugins; Node對象.getElementsByTagName; Node對象.getElementsByTagNameNS; Node對象.getElementsByClassName; HTMLTableElement對象.tBodies; HTMLTableElement對象.children; HTMLTableElement對象.rows; HTMLTableRowElement對象.cells; HTMLMapElement對象.areas;
// IE678 返回具有HTMLCollection特征(有namedItem方法)的[object Object]對象
// IE9、10、11返回HTMLCollection
// FF30.0、Chrome返回NodeList document.getElementsByName; // IE678 返回具有NodeList特征(無namedItem方法)的[object Object]對象
// IE9、10、11、FF、Chrome均返回NodeList
Node對象.childNodes; // IE5678 返回具有HTMLCollection特征(有namedItem方法)的[object Object]對象 // IE9、10返回[object HTMLCollection] // IE11、Chrome返回[object HTMLAllCollection] // FF30.0返回[object HTML document.all class] document.all;

 1. 總體來說Chrome的實現更接近W3C規范;

 2. HTMLAllCollection、HTMLCollection和[object HTML document.all class]功能沒什么區別,只是類型不同而已;

 3. 由于document.getElementsByName在不同的瀏覽器中返回不同類型的對象,因此推薦使用[{Number} 索引]的方法來訪問集合元素會省心一些;

 4. 題外話:children屬性僅獲取nodeType為1的元素,而childNodes會將所有子元素的包含進來;

 5. 注意:IE9、10、11的HTMLCollection與其他瀏覽器的HTMLCollection可不相同哦,具體請看下一節吧!

 

三、同名不同性——IE下怪異的HTMLCollection               

  假如大家看過《JS魔法堂:追憶那些原始的選擇器》,應該會了解到在IE5678下,document.all會返回一個類函數對象,也就是上文說到的帶有HTMLCollection特征的[object Object]對象。其實IE這一傳統一直延續到IE11,這就導致IE9、10、11下的HTMLCollection與W3C標準出現同名而不同性質的問題了。

  何為類函數?

純屬本人私自定義而已,用于指那些擁有函數的特征,但instanceof Function卻返回false的對象。

     真心想對IE說一句,你這么吊,你媽媽知道嗎?

 

四、StaticNodeList——偽裝成NodeList的小子                

  從IE8開始就多了個querySelectorAll選擇器方法。具體行為如下:

// IE8返回 [object Object], IE9+和chrome、FF就返回[object NodeList]
var nodes =  document.querySelectorAll('*');

// IE8返回 空集合[object Object],IE9+和chrome、FF就拋至少是1個函數入參的異常
nodes = document.querySelectorAll();

// 各瀏覽器均拋SyntaxError異常
nodes = document.querySelectorAll('') 或 document.querySelectorAll(非字符串類型入參);

 

大家不要被瀏覽器返回的NodeList所蒙騙,其實querySelectorAll返回的是StaticNodeList對象。其特征與NodeList基本無異,唯一的區別就是StaticNodeList是不會實時同步DOM樹變化,因此在polyfill querySelectorAll的時候就不用考慮實時同步DOM樹變化的問題了。 

 

五、HTMLOptionsCollection——HTMLCollection的子類            

  HTMLSelectElement對象.options會返回一個HTMLOptionsCollection集合對象,集合內存儲HTMLOptionElement類型的元素。HTMLOptionsCollection類型除了父類HTMLCollection的特征外,還有如下成員方法、屬性可用。

add({HTMLOptionElement} opt[, {HTMLOption | Number} before]); // 將選項元素加入到集合的最后,或指定的元素(位置)的后面
remove({Number} index);// 刪除指定位置的選項
selectedIndex; // 當前選中項的索引,從0開始

 

六、HTMLFormControllersCollection——HTMLCollection的子類         

  HTMLFormElement對象.elements會返回一個HTMLFormControllersCollection集合對象,集合內存儲各種表單元素。它特別之處是通過點屬性獲取id或name匹配的元素時,一般的HTMLCollection集合對象在即使有多個匹配的元素的情況下,僅返回首個匹配的元素;而HTMLFormControllersCollection,在有一個匹配的元素時就返回該元素,若有多個匹配的元素則返回一個RadioNodeList集合對象。

 

七、RadioNodeList——NodeList的子類                      

  初看RadioNodeList很有可能以為集合元素就是單選表單元素,其實RadioNodeList可以存儲任意類型的表單元素。不過其value屬性就值顯示其中被選中的單選項表單元素的value值,若沒有單選項表單元素,或沒有選中單選項表單元素,那么value值為空字符串。

 

八、HTMLAllCollection——HTMLCollection的子類               

  IE11、Chrome開始,document.all將返回HTMLCollection子類HTMLAllCollection的對象,其行為特征和HTMLCollection一致。但IE11中的HTMLAllCollection還可以當作函數使用,具體請看本文的第三節。

 

九、NamedNodeMap——無序Attr元素集合                    

  HTMLElement對象.attributes會返回NamedNodeMap集合對象,內部保存的是[object Attr]類型的對象。NamedNodeMap和HTMLCollection、NodeList不同,因為它是無序集合,雖然可以通過數字類型的下標索引訪問NamedNodeMap集合中的元素,但該索引值并不真實代表元素在集合中的位置。下面是NamedNodeMap的成員方法:

[{String} 屬性名]
item({Number | String} 索引) getNamedItem(); //通過名稱返回指定的屬性節點 getNamedItemNS(); //通過名稱和命名空間返回指定的屬性節點 setNamedItem(); //通過名稱設置指定的屬性節點 setNamedItemNS(); //通過名稱和命名空間設置指定的屬性節點 removeNamedItem(); //通過名稱刪除指定的屬性節點 removeNamedItemNS(); //通過名稱和命名空間刪除指定的屬性節點

  注意:HTMLElement對象.attributes僅返回顯示屬性(簡單地說就是直接寫在html標簽上的屬性,或通過setAttribute設置的屬性,具體請看《JS魔法堂:不要再被Attribute和Property困擾我們了》)

 

十、DOMTokenList——HTML5新特性classList的類型哦!        

  用過classList的都知道它大大提高了我們設置css類的效率,但IE10以下卻不支持,polyfill可以幫我們一把。但在polyfill前,我們應該先了解清楚classList的類型DOMTokenList的特征。

  1. 只讀

  2. 實時同步相應元素的className屬性值的變化

  3. 擁有以下方法和屬性

 {Undefined} add({String} class); // 已存在的類不會被重復添加
 {Undefined}  remove({String} class)
 {Undefined}  toggle({String} class)
 {Boolean} contains({String} class); //檢查是否有指定的類
 item({Number} 索引); //通過索引獲取指定位置的類
 length; //表示類的個數
 // 無法通過[{Number} 索引]的方式來設置類,只能通過該方式來獲取類

  那么現在我們就著手polyfill吧,注意難點在實時同步這一塊,解決辦法就是用onpropertychange來監聽className的變化(想了解更多,請看《JS魔法堂:DOM世界的觀察者》)

function polyfillClassList(el){
  var r = /\s+/, cls = el.className, _inner  =  cls ? cls.trim().split(r) : [];
  var listener = function(e){
    if (e.propertyName !== 'className') return void 0;

    var cLst = el.classList, oLen = _inner.length, cls=  el.className;
    _inner  =  cls ? cls.trim().split(r) : [];
    var len = (cLst.length = _inner.length);
    for (var i = 0, maxLen = Math.max(oLen, len); i < maxLen; ++i){
       if (i < len){
           cLst[i] = _inner[i])
       } else {
           delete cLst[i];
       }
    }    
  };
  el.attachEvent('onpropertychange', listener);
  el.classList = {
     length: _inner.length,
     item: function(index){
        return _inner[index] || null;
     },
     add: function(cls){
       // 省略檢查cls值是否有效的代碼
       if (this.contains(cls)) return void 0;
  
       el.detachEvent('onpropertychange',  listener);
       el.className += ' ' + cls;
       _inner.push(cls);
       this[this.length++] = cls;
       el.attachEvent('onpropertychage', listener);
     },
     remove: function(cls){
       // 省略檢查cls值是否有效的代碼
       if (!this.contains(cls)) return void 0;

       el.detachEvent('onpropertychange',  listener);
       el.className = el.className.replace(new RegExp('\\b' + cls + '\\b', 'i'), '').trim();
       _inner.splice(_inner.indexOf(cls), 1);
       --this.length;
       el.attachEvent('onpropertychage', listener);
     },
     toggle: function(cls){
       // 省略檢查cls值是否有效的代碼
       this[this.contains(cls) ? 'remove' : 'add'](cls);
     },
     contains: function(cls){
       // 省略檢查cls值是否有效的代碼
       return el.className.search(new RegExp('\\b' + cls + '\\b', 'i')) >= 0;
     },
     toString: function(){
       return _inner.toString();
     }
  };  

  // 初始化classList[{Number} 索引]獲取Attr元素
  for (var i = 0, len = _inner.length; i < len; ++i ){
    el.classList[i] = _inner[i];
  }
}        

由于當原生的add、remove、contains和toggle方法的入參值包含空格時,會拋出InvalidCharacterError,因此在polyfill時也要做相應的檢查和拋出異常

// 模擬InvalidCharacterError類
var InvalidCharacterError =  function(msg){
   this.code = 5;
   this.message = msg;
   this.name = 'InvalidCharacterError';
};
InvalidCharacterError.prototype = DOMException;


// 檢查入參并拋異常
// @param {String} methodName add、remove等方法名
// @param {String} cls css類
var check = function(methodName, cls){
  var msgTpl = ["Failed to execute '", , "' on 'DOMTokenList': The token provided ('", ,"') contains HTML space characters, which are not valid in tokens."];
  if (/\s+/.test(cls)){
     throw new InvalidCharacterError((msgTpl[1] = methodName, msgTpl[3] = cls, msgTpl).join(''));
  }
};

   更多關于異常處理、Error和Exception的信息請留意《JS魔法堂:異常處理并不那么簡單》

 

十一、DOMStringMap類型——HTML5新特性dataset的類型哦!  

  IE11開始支持 HTML5 JS API的dataset,它是就專門用來操作自定義特性(custom attribute,屬性的分類請看《JS魔法堂:特性、屬性,傻傻分不清楚》)的對象,其類型為DOMStringMap,從名稱可知其為字符串字典。下面結合dataset說明其特點吧,具體如下:

  ①. dataset針對以"data-"開頭的自定義特性操作;

  ②. 通過形如dataset.rawData獲取data-raw-data的屬性值;

  ③. 通過形如dataset.rawData = 'hello world!'給data-raw-data的屬性賦值;

  ④. 通過形如delete dataset.rawData刪除屬性data-raw-data;

  ⑤. 通過for in 遍歷dataset的屬性;

  ⑥. 屬性值必須或將自動轉換為String類型;

  ⑦. 其實它就是除了setAttribute、getAttribute等操作自定義特性的另一個接口而已,而且效率比get/setAttribute低,但大大簡化操作代碼。

  另外,JQuery中也有一個data函數,那么它跟以"data-"開頭的自定義特性有什么關聯呢?

html:<div id="div" data-raw="raw"></div>,使用jquery-1.10.2

        var $el = $('#div'), el = $el[0]; 

        function log(){
            console.log($el.data('raw'));
            console.log(el.dataset['raw']);
            console.log(el.outerHTML);
        }
        log();
     // 輸出:
     // raw
     // raw
// <div id="div" data-raw="raw"></div> $el.data(
'raw', '$'); log(); $el.data('raw', 'raw');
     // 輸出:
     // $
     // raw
     // <div id="div" data-raw="raw"></div>
el.dataset.raw
= 'dataset'; log(); el.dataset.raw = 'raw';
     // 輸出:
     // raw
     // dataset
     // <div id="div" data-raw="dataset"></div>
delete el.dataset.raw; log();
// 輸出:
// raw
// undefined
// <div id="div"></div> el.dataset.newRaw
= 'newRaw'; console.log($el.data('newRaw')); // 輸出newRaw

  從上面的實例可知:

    調用JQuery的data函數訪問屬性時,它會在庫內部的特性映射表中尋找同屬性名的鍵值對,沒有則采取與dataset相同的方式獲取屬性值,若成功則將在特性映射表中新建一個鍵值對,然后后續的訪問和賦值操作均僅僅針對該鍵值對。賦值操作時,僅僅在特性映射表中新建鍵值對,并不會賦值到標簽對應的"data-*"特性中。

    為何JQuery要設計成這樣呢?因為dataset的自定義特性值必須為String類型,賦予其他類型時會發生隱式類型轉換,不便于暫存對象、數組等數據。JQuery這種算是折中的做法吧,所以用JQuery的data API操作自定義特性時最好不要跟dataset或get/setAttribute等原生API混合用咯。

    

  本節參考:《HTML5自定義屬性對象Dataset 簡介

 

十二、 總結                              

   其實DOM的集合又何止上述的這些呢,在后續的日子里我會邊學習邊完善本文的,謝謝收看!

   尊重原創,轉載時請注明來自:http://www.cnblogs.com/fsjohnhuang/p/3819165.html ^_^肥仔John


文章列表




Avast logo

Avast 防毒軟體已檢查此封電子郵件的病毒。
www.avast.com


arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()