一、前言                                                                                                   

首先这里说的原始选择器是指除 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 = ;
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')[];
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; //
document.getElementsByClassName('inner').length; // 0 // IE5678
document.getElementsByTagName('form').length; // 2,dom树有嵌套的form节点
document.getElementById('inner'); // 1,dom树有嵌套的form节点
document.getElementsByName('inner').length; //

2. form节点下表单节点的差异

通过 form元素.length 可获取其下的 input节点 个数,通过 form元素[{Number} 索引] 获取指定位置的 input元素 。

// Webkit和Molliza
document.form[].length; // 2 // IE5678
document.form[].length; //
document.getElementsByTagName('form')[].length; // undefined,非嵌套的form节点.length没有input节点时返回0,而嵌套的form节点.length必定返回undefined // IE9
document.form[].length; //
document.form[].length; //

写到这里我想有人会说哪有人会写嵌套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 = , 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 = , 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 = ;
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 ; if (node.tagName !== 'form' && node.length && node[]){
for (var i = node.length - ; i >= ; --i){
wraperFactory(node[i]);
}
}
else{
var ns = !node.ownerDocument ? htmlDocSelectors : htmlElSelectors
, i = , currNS, currWraper;
while (currNS = ns[i++]){
if (currWraper = nsWrapers[currNS]){
node[currNS] = currWraper(node);
}
}
}
}; (! + [,]) && wraperFactory(doc);
}(window, document);

其中关于通过 (!+[,]) 判断IE5678的黑魔法我想大家早已从司徒正美的blog那听闻过了,但底层到底是怎样换算出来的呢?我们可以通过后面的《JS魔法堂:隐式类型转换的背后》来一起探讨一下!

十九、总结                                 

本来没想写这么多,但一边写一边找资料来尽量使内容完善,自己也得益不少。当然,内容上依旧不全面,望大家一起补充,一起探讨^_^!

尊重原创,转载请注明:http://www.cnblogs.com/fsjohnhuang/p/3811202.html

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

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

    一.前言 大家先看看下面的js,猜猜结果会怎样吧! 可选答案: ①. 获取id属性值为id的节点元素 ②. 抛namedItem is undefined的异常 var nodes = documen ...

  2. JS魔法堂:不完全国际化&本地化手册 之 理論篇

    前言  最近加入到新项目组负责前端技术预研和选型,其中涉及到一个熟悉又陌生的需求--国际化&本地化.熟悉的是之前的项目也玩过,陌生的是之前的实现仅仅停留在"有"的阶段而已. ...

  3. JS魔法堂:不完全国际化&本地化手册 之 实战篇

    前言  最近加入到新项目组负责前端技术预研和选型,其中涉及到一个熟悉又陌生的需求--国际化&本地化.熟悉的是之前的项目也玩过,陌生的是之前的实现仅仅停留在"有"的阶段而已. ...

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

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

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

    一.前言 我们一般使用方式为 <link type="text/css" rel="stylesheet" href="text.css&quo ...

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

    一.前言 在<JS魔法堂:jsDeferred源码剖析>中我们了解到img元素加载失败可以作为函数异步执行的优化方案,本文打算对img元素的加载行为进行更深入的探讨. 二.资源加载的相关属 ...

  7. JS魔法堂:jsDeferred源码剖析

    一.前言 最近在研究Promises/A+规范及实现,而Promise/A+规范的制定则很大程度地参考了由日本geek cho45发起的jsDeferred项目(<JavaScript框架设计& ...

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

    一.前言 或许你和我一样都曾经被下面的代码所困扰 var el = document.getElementById('dummy'); el.hello = "test"; con ...

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

    一.前言 什么是doctype?其实我们一直使用,却很少停下来看清楚它到底是什么,对网页有什么作用.本篇将和大家一起探讨那个默默无闻的doctype吧! 二.什么是doctype doctype或DT ...

随机推荐

  1. jackson 注脚学习参考

    (1)初级我们从几个简单的使用场景开始:重命名属性,忽略属性,以及修改属性所使用的类型.注意:下面的例子仅仅显示了成员属性(field properties),注解同样也可以用在成员方法(getter ...

  2. Dell U2913WM使用感受

    21:9比例,本来想代替双屏的,一周用下来还是不适应,如何能弯成曲面就爽了.感觉最舒服的还是以前19寸5:4双屏,点距大. 还尝试在旁边立个23寸,看了15分钟就受不了,头晕. 漏光,还行. 加了个A ...

  3. 如何在C语言中调用Swift函数

    在Apple官方的<Using Swift with Cocoa and Objectgive-C>一书中详细地介绍了如何在Objective-C中使用Swift的类以及如何在Swift中 ...

  4. WPF Wonders: Transformations (and Robots!)

    indows Presentation Framework (WPF) gets a lot of mileage out of being layered on top of DirectX, in ...

  5. 【转】微信公众账号 Senparc.Weixin.MP SDK 开发教程 索引

    微信公众账号 Senparc.Weixin.MP SDK 开发教程 索引 Senparc.Weixin.MP SDK从一开始就坚持开源的状态,这个过程中得到了许多朋友的认可和支持. 目前SDK已经达到 ...

  6. IIS服务器下做301永久重定向设置方法

    实现方法如下: 1.新建一个站点,对应目录如E:\wwwroot\301web.该目录下只需要1个文件,即index.html或者加个404.htm.绑定要跳转的域名,如图: 2.在IIS中选中刚才我 ...

  7. C#调用JAVA接口WSSE方式用WebClient方式

    C#读取JAVA的WSSE接口的调用代码: 用webclient 方式: /// <summary> /// 调用java cxf ws_security加密的服务wcf客户端对应的加密类 ...

  8. 使用git提交内容到网盘

    1.创建版本库(注意勾选纯版本库的选项) 2.客户端获取版本库代码 3.提交及获取 master是git默认的主要分支(主干),适合单人独自开发.多人开发时可以给每个人创建一个分支 参考资料: htt ...

  9. C#壓縮文件幫助類 使用ICSharpCode.SharpZipLib.dll

    using ICSharpCode.SharpZipLib.Checksums; using ICSharpCode.SharpZipLib.Zip; using System; using Syst ...

  10. 图解 & 深入浅出 JavaWeb:Servlet 再说几句

    Writer      :BYSocket(泥沙砖瓦浆木匠) 微         博:BYSocket 豆         瓣:BYSocket FaceBook:BYSocket Twitter   ...