本文介绍一个自定义的 JavaScript 函数,旨在实现原生 getElementsByClassName 的功能,同时支持指定父元素范围和子元素标签名过滤。文章深入解析了 HTMLCollection 的 length 属性缓存机制以提升循环性能,对比了数组 push 与直接赋值在添加元素时的效率差异,并提供了兼容 IE5 浏览器的特殊处理方案。
JavaScript 实现支持多 Class 与指定范围的 getElementsByClassName 函数
首先展示一段代码,该实现支持查询多个 class 名称,并允许在指定的父元素范围内进行搜索。
具体实现代码如下:
/*
* 根据元素的 className 获取对应的元素集合
* @param fatherId 父元素的 ID,若未提供则默认为 document
* @param tagName 子元素的标签名
* @param className 由空格分隔的 className 字符串
*/
function getElementsByClassName(fatherId,tagName,className){
node = fatherId&&document.getElementById(fatherId) || document;
tagName = tagName || "*";
className = className.split(" ");
var classNameLength = className.length;
for(var i=0,j=classNameLength;i<j;i++){
//创建匹配类名的正则表达式
className[i]= new RegExp("(^|\\s)" + className[i].replace(/\-/g, "\\-") + "(\\s|$)");
}
var elements = node.getElementsByTagName(tagName);
var result = [];
for(var i=0,j=elements.length,k=0;i<j;i++){//缓存length属性
var element = elements[i];
while(className[k++].test(element.className)){//优化循环
if(k === classNameLength){
result[result.length] = element;
break;
}
}
k = 0;
}
return result;
} 功能测试与原生方法对比
接下来进行功能测试:
测试用的 HTML 结构如下:
<div id="container">
<span class="aaa zzz ccc"></span>
<div class="aaa bbb ccc"></div>
</div>
<div class="aaa bbb ccc"></div> 测试脚本代码如下:
window.onload = function(){
alert(getElementsByClassName(document,"div","aaa ccc").length);//2
alert(getElementsByClassName("container","div","aaa ccc").length);//1
alert(getElementsByClassName("container","span","aaa zzz").length);//1
} 上述测试均得到了正确的结果。
关于原生 getElementsByClassName 方法的讨论
有人可能会质疑,既然原生方法调用效率最高,且多数现代浏览器已实现该方法,为何此处不优先调用原生方法再回退到自定义实现?
确实,原生方法效率极高且支持多 class 查询,但其最大缺陷在于不支持类似 getElementsByClassName("container","div","aaa ccc") 这种在指定标签内查找特定 class 元素的语法。
鉴于此限制,本实现选择舍弃直接调用原生方法。
循环优化与数组添加效率分析
关于循环优化的细节
在代码中,我们将数组的 length 属性进行了缓存,这有助于提升执行效率。这里存在一个容易被忽视的问题:访问数组的 length 属性与访问 HTMLCollection 的 length 属性有着本质区别。数组的 length 是普通属性,访问时无额外开销;而 HTMLCollection 是一个随 DOM 结构自动变化的实体对象,每次访问其属性(包括 length)时,都会对 DOM 内所有节点进行一次完整匹配更新。因此,每次访问 HTMLCollection 对象的 length 都会导致集合对象更新,带来巨大的性能消耗。通常情况下,对 HTMLCollection 进行循环操作时,建议务必缓存 length 属性。
额外的性能发现
这里涉及向数组添加元素时不同方式的效率对比。
对比代码如下:
对比实现如下:
//方式一
var arr = [];
var start = new Date();
for(var i=0;i<100000;i++){
arr.push(i);
}
//方式二
var arr = [];
var start = new Date();
for(var i=0;i<100000;i++){
arr[arr.length]=i;
} 大家猜猜哪种方式效率更高?经过实际测试,第二种方式(直接通过索引赋值)的效率高于第一种(使用 push 方法)。
IE5 兼容性处理方案
兼容性提醒:
当前版本不兼容 IE5 浏览器,以下是相关解释:
1. 从极低的 IE5 市场份额统计中,再减去那些专门测试 IE5 的点击量和专业程序员对 IE5 的好奇心,剩下的关注点可能仅剩 IE5 被废弃后的“灰烬”。
2. 你需要相信,这些“灰烬”的利用价值趋近于零。
3. 如果上述理由仍无法说服你,那么我想说:你很专业!既然这样,是否也应该一并实现 IE3、IE2、IE?、FF1、NN4 等更古老浏览器的兼容?
坦白说,我也很难完全说服自己,因此还是提供一个针对 IE5 的解决方案吧:
IE5 不支持 getElementsByTagName("*") 这种写法,因此需要对此进行特殊处理:
var elements = (tagName==='*'&&node.all)?node.all:node.getElementsByTagName(tagName);
至此,该函数简单且实用,代码中已添加详细注释,理解起来应该没有太大问题。