JS魔法堂:追忆那些原始的选择器

一、前言                                                                                                   

首先这里说的原始选择器是指除 querySelector 、 querySelectorAll 外的其他选择器。从前我只使用 getElementById 获取元素并没有觉得有什么问题,但随着参与项目的前端规模逐步扩大,踩的坑就越来越多,于是将踩过的和学习过的经验教训记录在这里,供以后好查阅。

二、HTMLDocument和HTMLElement下的常规选择器                                    

1. HTMLDocument的选择器: getElementById 、 getElementsByName 、 getElementsByTagName、 getElementsByClassName

2. HTMLElement的选择器: getElementsByTagName 、 getElementsByClassName

三、被遗忘的小伙伴getElementsByClassName                                            

对于像我这样被专注于管理类后台系统开发的伪前端码农来说, getElementsByClassName 确实是见都没见过,因为IE5678原生就不支持它。但从命名可知其功能就是,它是通过类名选择元素。那么我们就可以polyfill一下了。

/** 通过类名选择元素
 * @method
 * @param {Node} node DOM元素
 * @param {String} cls 类名
 */
var _getElementsByClassName = function(node, cls){
    var seed = node.childNodes, nodes = [], i = 0, node;
    while(node = seed[i++]){
        if (node.nodeType === 1){
            node.className.search(new RegExp(‘\\b‘ + cls + ‘\\b‘, ‘i‘)) >= 0 && nodes.push(node);
            nodes = nodes.concat(_getElementsByClassName(node, cls));
        }
    }
    return nodes;
};

document.getElementsByClassName = function(cls){  return _getElementsByClassName(cls);};

四、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;
};

五、无法更改执行上下文的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嘛),其它用法能不用就坚决不用吧。

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。下面我们再罗列出

九、隐藏的武士刀二: 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){
   /** 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 = null;
        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;
    };
     // 选择器加工工厂对象
    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);){}
            }

            node = removeNestForm(node);
            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);

            nodes = removeNestForm(nodes);
            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);

            nodes = removeNestForm(nodes);
            wraperFactory(nodes);
            return nodes;
        };
    };
    /** 通过类名选择元素
     * @method
     * @param {Node} node DOM元素
     * @param {String} cls 类名
     */
    var _getElementsByClassName = function(node, cls){
        var seed = node.childNodes, nodes = [], i = 0, node;
        while(node = seed[i++]){
            if (node.nodeType === 1){
                node.className.search(new RegExp(‘\\b‘ + cls + ‘\\b‘, ‘i‘)) >= 0 && nodes.push(node);
                nodes = nodes.concat(_getElementsByClassName(node, cls));
            }
        }

        return nodes;
    };
    nsWrapers.getElementsByClassName = function(node){
        var host = node;

        return function(cls){
            var nodes = _getElementsByClassName(host, cls);

            nodes = removeNestForm(nodes);
            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

JS魔法堂:追忆那些原始的选择器

时间: 2024-10-26 13:15:36

JS魔法堂:追忆那些原始的选择器的相关文章

JS魔法堂:那些困扰你的DOM集合类型

一.前言 大家先看看下面的js,猜猜结果会怎样吧! 可选答案: ①. 获取id属性值为id的节点元素 ②. 抛namedItem is undefined的异常 var nodes = document.getElementsByName('dummyName'); var node = nodes.namedItem('id'); 答案是两种都有可能哦!document.getElementsByName在Chrome和FF30.0中返回NodeList(木有namedItem方法的),在IE

JS魔法堂:LINK元素深入详解

一.前言 我们一般使用方式为 <link type="text/css" rel="stylesheet" href="text.css"> 来引入外部层叠式样式文件,但LINK元素各属性的具体含义.资源加载行为等方面却了解不多,本文打算稍微深入一下. 由于内容较多,特设目录一坨: 二.到底有没有结束标签? 三.普通属性介绍 四.属性disabled详解 1. Attribute和Property的disabled  2. disab

JS魔法堂:元素克隆、剪切技术研究

原文:JS魔法堂:元素克隆.剪切技术研究 一.前言 当需要新元素时我们可以通过 document.createElement 接口来创建一个全新的元素,也可以通过克隆已有元素的方式来获取一个新元素.而在部分浏览器中,通过复制来获取新元素的效率比通过 document.createElement 方式的要高一些,具体的性能比较如下: 2% in IE8, but no change in IE6 and IE7 Up to 5.5% in Firefox 3.5 and Safari 4 6% i

JS魔法堂:IMG元素加载行为详解

一.前言 在<JS魔法堂:jsDeferred源码剖析>中我们了解到img元素加载失败可以作为函数异步执行的优化方案,本文打算对img元素的加载行为进行更深入的探讨. 二.资源加载的相关属性和事件 资源加载首先当然是确定资源位置的 src属性 .随之就是资源加载成功与否的 onload事件 和 onerror事件 ,对于IE5~10来说还多了一个 onreadystatechage事件 和与该事件相关联的 readyState属性 和 complete属性 . onload事件 ,当资源加载完

JS魔法堂:精确判断IE的文档模式by特征嗅探

一.前言 苦逼的前端攻城狮都深受浏览器兼容之苦,再完成每一项功能前都要左顾右盼,生怕浏览器不支持某个API,生怕原生API内含臭虫因此判断浏览器类型和版本号成了不可绕过的一道关卡,而特征嗅探是继浏览器探测后另一利器处理上述问题. 二.何为特征嗅探 从前我们都是通过对navigator.userAgent或navigator.appName两个属性值进行特定字符串匹配和萃取来区分浏览器类型和获取版本号的.但随着IE8提供可选的文档兼容性模式设置和各种加壳浏览器的出现,导致无法通过navigator

JS魔法堂:属性、特性,傻傻分不清楚

一.前言 或许你和我一样都曾经被下面的代码所困扰 var el = document.getElementById('dummy'); el.hello = "test"; console.log(el.getAttribute('hello')); // IE67下输出test,其他浏览器输出null “搞毛啊?”,苦逼的Jser对着浏览器大呼一声.然后就用下面蹩脚的方式草草处理掉了. function getAttr(el, prop){ return el[prop] || el

JS魔法堂:doctype我们应该了解的基础知识

一.前言 什么是doctype?其实我们一直使用,却很少停下来看清楚它到底是什么,对网页有什么作用.本篇将和大家一起探讨那个默默无闻的doctype吧! 二.什么是doctype doctype或DTD就是声明在文档首行,位于<html>前,用于告知浏览器该文档遵循那种级别的HTML或XHTML规范. 其声明格式如下: <!DOCTYPE① html② PUBLIC③ "公共标识符"④ "系统标识符"⑤> ①. dotype固定的起始部分 ②

JS魔法堂:浏览器模式和文档模式怎么玩?

一.前言 从IE8开始引入了文档兼容模式的概念,作为开发人员的我们可以在开发人员工具中通过“浏览器模式”和“文档模式”(IE11开始改为“浏览器模式”改成更贴切的“用户代理字符串”)品味一番,它的出现极大地方便了苦逼的前端攻城狮们适配各版本的IE,但jser们也不能完全信任它,因为它只是提供尽可能的文档模式模拟而已. 本篇大部分内容来源于官方解说:http://msdn.microsoft.com/library/cc288325(v=vs.85).aspx,并尽量融入个人平常工作中踩过的坑加以

JS魔法堂:判断节点位置关系

一.前言 在polyfill querySelectorAll 和写弹出窗时都需要判断两个节点间的位置关系,通过jQuery我们可以轻松搞定,但原生JS呢?下面我将整理各种判断方法,以供日后查阅. 二.祖孙关系 html <div id="ancestor"> <div id="parent"> <div id="son">son</div> </div> </div> &l