一、前言
首先這里說的原始選擇器是指除 querySelector 、 querySelectorAll 外的其他選擇器。從前我只使用 getElementById 獲取元素并沒有覺得有什么問題,但隨著參與項目的前端規模逐步擴大,踩的坑就越來越多,于是將踩過的和學習過的經驗教訓記錄在這里,供以后好查閱。
二、HTMLDocument和HTMLElement下的常規選擇器
1. HTMLDocument的選擇器: getElementById 、 getElementsByName 、 getElementsByTagName、 getElementsByClassName
2. HTMLElement的選擇器: getElementsByTagName 、 getElementsByClassName
三、被遺忘的小伙伴getElementsByClassName
對于像我這樣被專注于管理類后臺系統開發的偽前端碼農來說, getElementsByClassName 確實是見都沒見過,因為IE5678原生就不支持它。但從命名可知其功能就是,它是通過類名選擇元素。那么我們就可以polyfill一下了。
document.getElementsByClassName = function(cls){
var r = new RegExp('\\b' + cls + '\\b', 'i');
var seed = document.all, i = 0, nodes = [], node;
while (node = seed[i++]){
if (node.nodeType === 1){
node.className.search(r) >= 0 && nodes.push(node);
}
}
return nodes;
};
注意:上述的polyfill僅僅是表面填補泥而已,返回的為節點數組并非HTMLCollection類型對象,因此缺失只讀、實時同步、item和namedItem等特性。
四、IE567下getElementById的詭異行為
通過望文生義,getElementById理應只返回id屬性值匹配的元素,而IE8+、webkit和molliza也是這樣做的。但IE567卻不遵循這一法則,它們會獲取id屬性值或name屬性值匹配的元素,然后以第一個匹配的元素作為返回值。
示例:
html
<span name="dummy"></span> <div id="dummy"></div>
javascript
var node = document.getElementById("dummy"); // IE8+、Webkit和Molliza下均顯示div // IE567下顯示span console.log(node.tagName.toLocaleLowerCase());
針對上述IE的bug我們可以進行簡單的修復
var nativeGetById = document.getElementById; document.getElementById = function(id){
var node = nativeGetById.call(this, id); if (node && node.id !== id){ var nodes = document.all[id]; var i = 0; for (;(node = nodes && nodes[i++] || null, node && node.id !== id);){}
// 上面的for循環是把玩語法而已,效果和下面的一樣
// if (!nodes) return null;
// for (var len = nodes.length; i < len; ++i){
// node = nodes[i];
// if (node && node.id === id) break;
// }
}
return node; };
五、IE56789下getElementsByName的怪異行為
經踩坑發現在IE56789下使無法通過getElementsByName來獲取table、td、th、tr、tbody、thead、tfoot的對象引用,查閱W3C表示這些元素的固有屬性本來就沒有name,所以最初認為IE這一行為是正確的。但經過試驗發現同樣沒有name固有屬性的colgroup、caption和col卻能通過getElementsByName獲取,于是開始頭大了。然后轉向IE10+、Webkit和Molliza進行同樣的測試,均可成功獲取,于是判斷這是IE56789的怪異行為。
發現這一問題后我想到的是對IE56789下getElementsByName的返回值進行加工,將name屬性值匹配的table、td、th、tr、tbody、thead和tfoot對象都加上去,雖然這樣就解決了對象缺失的問題,但又引入了新的問題,那就是getElementsByName的返回值不再是HTMLCollection類型,因此失去了與文檔節點信息實時同步、只讀、item成員方法、namedItem成員方法的特性。
失去得顯然比得到的少,于是我決定不修復這一怪異行為。
六、無法更改執行上下文的this引用?
自從知道 Function.prototype.call、Function.prototype.apply和Fucntion.prototype.bind 后,鎖定執行上下文(EC)的this引用變得十分的簡單(具體的polyfill可瀏覽《一起Polyfill系統:Function.prototype.bind的四個階段》)。但倘若你想通過鎖定getElementById、getElementsByName的this引用,從而達到選擇根節點的動態變換,那將掉進另一個坑中。
錯誤的示例:
// 下面的代碼將會拋異常 var nativeGetId = document.getElementById; var a = document.getElementsByTagName('a')[0]; nativeGetId.call(a, 'innerImg');
根據現象推測,getElementId內部實現可能是針對特定的DOM對象而工作的,所以當強行改變this引用時,就會跑異常。
七、IE5678下選擇器的原型鏈上少了Function?
也許你看到這個標題的時候會認為這是不可能的事,因為 document.getElementById.call 是真實存在的呀。但 document.getElementById instanceof Function 居然返回false,現在頭大了吧。讓我們再通過下面對Function原型增強來驗證一下吧!
Function.prototype.just4Test = function(){ console.log('just4Test'); }; console.log(typeof document.getElementById.just4Test); // 返回undefined
事實證明IE5678下選擇器的原型鏈沒有Function,那選擇器就無法共享各種對Function原型的增強了,所以我們需要通過一層薄薄的封裝來處理。
// 以getElementsByName為例 var nativeGetByName = document.getElementsByName; document.getElementsByName = function(name){ return nativeGetByName.call(this, name); };
八、IE獨創的選擇器
上面說到的選擇器是各大瀏覽器廠商都支持,而IE獨創的選擇器我想大家都會想到是 document.all ,但這個類函數水可不淺,下面讓我們來踩一下吧!
// IE5678下,獲取NodeList,但在IE567中通過Object.prototype.toString.call()獲取內部類型時,返回的是[object Object] document.all[`id或name`]; // IE5678下,獲取的是指定索引值的元素HTMLElement通過Object.prototype.toString.call()獲取內部類型時,返回的是[object Object] document.all[{Number} 索引]; document.all(); // 獲取第一個元素(指定索引值的元素) document.all({Number} 索引); // 獲取第一個元素(指定索引值的元素) // IE567下,獲取id屬性值或name屬性值匹配的所有元素,返回一個有函數功能的[object Object]對象 document.all({String} id或name); document.all({String} id或name, {Number} 索引); // 獲取HTMLElement document.all({String} id或name)({Number} 索引); // 獲取HTMLElement // IE8下,獲取的是第一個匹配的元素HTMLElement通過Object.prototype.toString.call()獲取內部類型時,返回的是[object Object] document.all({String} id或name); document.all({String} id或name, 索引); // 拋異常 // IE5678,通過標簽名獲取匹配的所有元素,返回一個有函數功能的[objectg Object]對象 document.all.tags({String} tag); document.all.tags({String} tag)({Number} 索引); document.all.tags({String} tag)[{Number} 索引]; // IE5678,獲取指定位置的元素(HTMLElement) document.all.item(); // 獲取第一個元素 document.all.item({Number} 索引); // IE567,獲取id屬性值或name屬性值匹配的所有元素,返回一個有函數功能的[object Object]對象 document.all.item({String} id或name); // IE567,返回元素(HTMLElement) document.all.item({String} id或name, {Number} 索引); document.all.item({String} id或name)({Number} 索引); document.all.item({String} id或name)[{Number} 索引]; // IE8+,只返回第一個元素 document.all.item({String} id或name); // IE8+,只返回一個HTMLCommentElement對象 document.all.item({String} id或name, {Number} 索引); document.all.item({String} id或name)({Number} 索引); document.all.item({String} id或name)[{Number} 索引];
總結一句,若要使用那就使用 document.all[{String} id或name] 就好了(其他返回的是正常的NodeList嘛),其它用法能不用就堅決不用吧。
另外,除了document擁有all屬性外,其實直接繼承Node類型的都擁有all屬性,也就是說素有DOM對象均有all屬性用于獲取其所有子節點。
0級DOM武士刀
0級DOM:在W3C標準DOM起草前,由網景公司定義的節點操控API,并后來作為W3C標準的0級DOM規范。
九、隱藏的武士刀一: document.forms
無論是在w3c還是其他渠道查閱都被告知該函數用于獲取頁面上所有form元素,當然這點說得一點都沒有錯,但不夠深入。那么如何深入呢?那么就要從form的嵌套入手了。
html:
<form name="outer" id="outer"> <input type="text" name="outerInput"/> <form name="inner" id="inner" class="inner"> <input type="text" name="innerInput"/> </form> </form>
1. form元素個數差異
IE5678、Webkit和Molliza都會排除嵌套的form元素,而IE9會保留form元素。
// IE5678、Webkit和Molliza,會排除嵌套的form元素 document.forms.length; // 返回1 // IE9,保留嵌套的form元素 document.forms.length; // 返回2
通過在Chrome的調試工具可查看Webkit解析生成的DOM樹結構,是不生產嵌套的form元素的,并且將嵌套的form節點下的子節點提取到上一級。而在IE5678下,通過調試工具發現DOM樹中依然包含嵌套的form元素節點,但其下的子節點被提取到上一級。而IE9下的嵌套form節點在DOM樹中被完整的構建,因此不僅DOM中包含嵌套的form節點,而且其子節點并沒有被提取到上一級。
下面代碼級的驗證:
// Webkit和Molliza document.getElementsByTagName('form').length; // 1,dom樹沒有嵌套的form節點所以找不到 document.getElementById('inner'); // null,dom樹沒有嵌套的form節點所以找不到 document.getElementsByName('inner').length; // 0 document.getElementsByClassName('inner').length; // 0 // IE5678 document.getElementsByTagName('form').length; // 2,dom樹有嵌套的form節點 document.getElementById('inner'); // 1,dom樹有嵌套的form節點 document.getElementsByName('inner').length; // 0
2. form節點下表單節點的差異
通過 form元素.length 可獲取其下的 input節點 個數,通過 form元素[{Number} 索引] 獲取指定位置的 input元素 。
// Webkit和Molliza document.form[0].length; // 2 // IE5678 document.form[0].length; // 2 document.getElementsByTagName('form')[1].length; // undefined,非嵌套的form節點.length沒有input節點時返回0,而嵌套的form節點.length必定返回undefined // IE9 document.form[0].length; // 1 document.form[1].length; // 1
寫到這里我想有人會說哪有人會寫嵌套form的啊,確實能寫出這種html結構出來的,我也十分佩服。總結一句,真心請大伙不要嵌套form。下面我們再羅列出
下面是判斷嵌套form和排除的方法,但不建議為排除嵌套form而重寫document.getElementsByTagName等方法,因為會將原來為HTMLCollection或NodeList類型的返回對象,改為沒有實時同步特性的Array對象,何苦呢。。。。。。
/** IE5678中用于判斷是否為嵌套form * @method * @param {HTMLFormElement} form * @return {Boolean} */ var isNestForm = function(form){ var forms = document.forms, i = 0, curr; for (;(curr = forms[i++], curr && curr !== form);){} return !curr; }; var removeNestForm = function(node){ if (node === null || typeof node === 'undefined') return null; var ret = node; if (node.tagName && node.tagName.toLocaleLowerCase() === 'form'){ ret = isNestForm(node) ? null : node; } else if (node.length){ ret = []; for (var i = 0, len = node.length; i < len; ++i){ var tmp = node[i]; isNestForm(tmp) || ret.push(tmp); } } return ret; };
十、隱藏的武士刀二: document.links
獲取文檔中所有擁有href屬性的a和area對象的引用。但在IE5678中 document.links是個類函數,而在Webkit和Molliza中是個HTMLCollection對象。
// IE5678、Webkit和Molliza中獲取指定位置的元素對象
document.links[{Number} 索引];
// IE5678中獲取指定位置的元素對象 document.links({Number} 索引);
// Webkit和Molliza中通過id或name屬性值獲取元素對象
document.links[{String} id或name];
// IE5678中通過id或name屬性值獲取元素對象
document.links({String} id或name);
十一、隱藏的武士刀三: document.scripts
獲取文檔中所有script對象的引用。但從IE5678到Webkit、Molliza都包含以自閉合格式聲明的script對象 <script /> ,正確的聲明格式是 <script></script> 。
但在IE5678中 document.scripts是個類函數,而在Webkit和Molliza中是個HTMLCollection對象。在IE5678下的具體玩法如下:
// 獲取指定位置的元素對象
document.scripts[{Number} 索引];
document.scripts({Number} 索引);
十二、隱藏的武士刀四: document.styleSheets
獲取文檔中所有style和link的CSSStyleSheet類型對象的引用,與document.getElementsByTagName('style')和document.getElementsByTagName('link')獲取的是HTMLStyleElement類型對象是不同的,在IE5678中是一個類函數,Webkit和Molliza中是一個StyleSheetList類型對象(屬于NodeList類型,想了解跟多NodeList和HTMLCollection可留意另一篇《JS魔法堂:那些困擾你的DOM集合類型》)。由于涉及的邊幅過大,因此打算另開一篇《JS魔法堂:哈佬,css.js!》
十三、隱藏的武士刀五: document.anchors
獲取文檔中所有錨對象(HTMLAnchorElement)的引用。該方法在IE5678下返回的是一個類函數,在Webkit、Molliza下返回一個HTMLCollection對象。并且在IE5678和Webkit、Molliza的獲取的錨對象個數也不同。
html
<a href="javascript: void 0;">links</a> <a name="a1" id="b1">anchor1</a> <a name="a1" id="b2">anchor2</a> <a name="a3" id="b3">anchor3</a>
javascript
var anchors = document.anchors; // IE5678 anchors.length; // 返回4,包含links anchors[{Number|String} 索引]; // 返回指定位置的元素 anchors({String} id或name); // 返回第一個id或name匹配的元素 // Webkit、Molliza anchors.length; // 返回3 anchors[{Number|String} 索引]; // 返回指定位置的元素 anchors[{String} id或name]; // 返回第一個id或name匹配的元素
十四、隱藏的武士刀六: document.images
獲取文檔中所有img的對象引用。 該方法在IE5678下返回的是一個類函數,在Webkit、Molliza下返回一個HTMLCollection對象。
十五、隱藏的武士刀七: document.embeds
獲取文檔中所有embed的對象引用。該方法在IE5678下返回的是一個類函數,在Webkit、Molliza下返回一個HTMLCollection對象。
十六、隱藏的武士刀八: document.applets
獲取文檔中所有applet的對象引用。該方法在IE5678下返回的是一個類函數,在Webkit、Molliza下返回一個HTMLCollection對象。
十七、隱藏的武士刀九: document.plugins
效果和document.embeds一樣
十八、完整實現
這里對getElementById,getElementsByTagName,getElementsByName進行了封裝從而繼承Function,并polyfill了getElementsByClassName,并排除嵌套form的問題。
void function(global, doc){
// 選擇器加工工廠對象 var nsWrapers = {}; nsWrapers.getElementById = function(node){ var host = node; var nativeGetById = host.getElementById; /** 修復IE567下document.geElementById會獲取name屬性值相同的元素 * 修復IE5678下document.geElementById沒有繼承Function方法的詭異行為 * @method * @param {String} id * @return {HTMLElementNode|Null} */ return function(id){ var node = nativeGetById.call(host, id); if (node && node.id !== id){ var nodes = doc.all[id]; var i = 0; for (;(node = nodes && nodes[i++] || null, node && node.id !== id);){} } wraperFactory(node); return node; }; }; nsWrapers.getElementsByName = function(node){ var host = node; var nativeGetByName = host.getElementsByName; /** 修復IE5678下document.geElementsByName沒有繼承Function方法的詭異行為 * @method */ return function(tag){ var nodes = nativeGetByName.call(host, tag); wraperFactory(nodes); return nodes; }; }; nsWrapers.getElementsByTagName = function(node){ var host = node; var nativeGetByTagName = host.getElementsByTagName; /** 修復IE5678下document.geElementsByTagName沒有繼承Function方法的詭異行為 * @method */ return function(tag){ var nodes = nativeGetByTagName.call(host, tag); wraperFactory(nodes); return nodes; }; }; nsWrapers.getElementsByClassName = function(node){ var host = node; return function(cls){
var r = new RegExp('\\b' + cls + '\\b', 'i');
var seed = host.all, i = 0, nodes = [], node;
while (node = seed[i++]){
if (node.nodeType === 1){
node.className.search(r) >= 0 && nodes.push(node);
}
}
wraperFactory(nodes); return nodes; }; }; var htmlElSelectors = ['getElementsByTagName', 'getElementsByClassName']; var htmlDocSelectors = htmlElSelectors.concat(['getElementById', 'getElementsByName']); var wraperFactory = function(node){ if (!node) return void 0; if (node.tagName !== 'form' && node.length && node[0]){ for (var i = node.length - 1; i >= 0; --i){ wraperFactory(node[i]); } } else{ var ns = !node.ownerDocument ? htmlDocSelectors : htmlElSelectors , i = 0, currNS, currWraper; while (currNS = ns[i++]){ if (currWraper = nsWrapers[currNS]){ node[currNS] = currWraper(node); } } } }; (! + [1,]) && wraperFactory(doc); }(window, document);
其中關于通過 (!+[1,]) 判斷IE5678的黑魔法我想大家早已從司徒正美的blog那聽聞過了,但底層到底是怎樣換算出來的呢?我們可以通過后面的《JS魔法堂:隱式類型轉換的背后》來一起探討一下!
十九、總結
本來沒想寫這么多,但一邊寫一邊找資料來盡量使內容完善,自己也得益不少。當然,內容上依舊不全面,望大家一起補充,一起探討^_^!
尊重原創,轉載請注明:http://www.cnblogs.com/fsjohnhuang/p/3811202.html
文章列表