JS魔法堂:追憶那些原始的選擇器

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 

相關文章
相關標籤/搜索