一、前言
大家先看看下面的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
文章列表