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
欢迎添加我的公众号一起深入探讨技术手艺人的那些事!
如果您觉得本文的内容有趣就扫一下吧!捐赠互勉!