文章出處

一、前言                                                                                                   

  首先這里說的原始選擇器是指除 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 


文章列表




Avast logo

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


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

    IT工程師數位筆記本

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