1、前言 javascript
首先這裏說的原始選擇器是指除 querySelector 、 querySelectorAll 外的其餘選擇器。從前我只使用 getElementById 獲取元素並無以爲有什麼問題,但隨着參與項目的前端規模逐步擴大,踩的坑就愈來愈多,因而將踩過的和學習過的經驗教訓記錄在這裏,供之後好查閱。css
2、HTMLDocument和HTMLElement下的常規選擇器 html
1. HTMLDocument的選擇器: getElementById 、 getElementsByName 、 getElementsByTagName、 getElementsByClassName 前端
2. HTMLElement的選擇器: getElementsByTagName 、 getElementsByClassName java
3、被遺忘的小夥伴getElementsByClassName node
對於像我這樣被專一於管理類後臺系統開發的僞前端碼農來講, getElementsByClassName 確實是見都沒見過,由於IE5678原生就不支持它。但從命名可知其功能就是,它是經過類名選擇元素。那麼咱們就能夠polyfill一下了。web
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等特性。數組
4、IE567下getElementById的詭異行爲 瀏覽器
經過望文生義,getElementById理應只返回id屬性值匹配的元素,而IE8+、webkit和molliza也是這樣作的。但IE567卻不遵循這一法則,它們會獲取id屬性值或name屬性值匹配的元素,而後以第一個匹配的元素做爲返回值。app
示例:
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; };
5、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成員方法的特性。
失去得顯然比獲得的少,因而我決定不修復這一怪異行爲。
6、沒法更改執行上下文的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引用時,就會跑異常。
7、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); };
8、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規範。
9、隱藏的武士刀一: 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元素個數差別
IE567八、Webkit和Molliza都會排除嵌套的form元素,而IE9會保留form元素。
// IE567八、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; };
10、隱藏的武士刀二: document.links
獲取文檔中全部擁有href屬性的a和area對象的引用。但在IE5678中 document.links是個類函數,而在Webkit和Molliza中是個HTMLCollection對象。
// IE567八、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);
11、隱藏的武士刀三: document.scripts
獲取文檔中全部script對象的引用。但從IE5678到Webkit、Molliza都包含以自閉合格式聲明的script對象 <script /> ,正確的聲明格式是 <script></script> 。
但在IE5678中 document.scripts是個類函數,而在Webkit和Molliza中是個HTMLCollection對象。在IE5678下的具體玩法以下:
// 獲取指定位置的元素對象
document.scripts[{Number} 索引];
document.scripts({Number} 索引);
12、隱藏的武士刀四: document.styleSheets
獲取文檔中全部style和link的CSSStyleSheet類型對象的引用,與document.getElementsByTagName('style')和document.getElementsByTagName('link')獲取的是HTMLStyleElement類型對象是不一樣的,在IE5678中是一個類函數,Webkit和Molliza中是一個StyleSheetList類型對象(屬於NodeList類型,想了解跟多NodeList和HTMLCollection可留意另外一篇《JS魔法堂:那些困擾你的DOM集合類型》)。因爲涉及的邊幅過大,所以打算另開一篇《JS魔法堂:哈佬,css.js!》
十3、隱藏的武士刀五: 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匹配的元素
十4、隱藏的武士刀六: document.images
獲取文檔中全部img的對象引用。 該方法在IE5678下返回的是一個類函數,在Webkit、Molliza下返回一個HTMLCollection對象。
十五、隱藏的武士刀七: document.embeds
獲取文檔中全部embed的對象引用。該方法在IE5678下返回的是一個類函數,在Webkit、Molliza下返回一個HTMLCollection對象。
十6、隱藏的武士刀八: document.applets
獲取文檔中全部applet的對象引用。該方法在IE5678下返回的是一個類函數,在Webkit、Molliza下返回一個HTMLCollection對象。
十7、隱藏的武士刀九: document.plugins
效果和document.embeds同樣
十8、完整實現
這裏對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魔法堂:隱式類型轉換的背後》來一塊兒探討一下!
十9、總結
原本沒想寫這麼多,但一邊寫一邊找資料來儘可能使內容完善,本身也得益很多。固然,內容上依舊不全面,望你們一塊兒補充,一塊兒探討^_^!
尊重原創,轉載請註明:http://www.cnblogs.com/fsjohnhuang/p/3811202.html