zoukankan      html  css  js  c++  java
  • JS魔法堂:追忆那些原始的选择器

    一、前言                                                                                                   

      首先这里说的原始选择器是指除 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 

  • 相关阅读:
    Mac上的USB存储设备使用痕迹在新版操作系统有所变化
    Beware of the encrypted VM
    A barrier for Mobile Forensics
    Second Space could let suspect play two different roles easily
    Take advantage of Checkra1n to Jailbreak iDevice for App analysis
    Find out "Who" and "Where"
    Where is the clone one and how to extract it?
    Downgrade extraction on phones running Android 7/8/9
    高版本安卓手机的取证未来
    How to extract WeChat chat messages from a smartphone running Android 7.x or above
  • 原文地址:https://www.cnblogs.com/fsjohnhuang/p/3811202.html
Copyright © 2011-2022 走看看