前言
遍历数组或对象是一名程序员的基本素养之一. 然而遍历却不是一件简单的事,优秀的程序员知道怎么去选择合适的遍历方法,优化遍历效率. 本篇将带你走进JavaScript遍历的世界,享受分析JS循环的快感. 本篇所有代码都可以直接运行,希望您通读本篇后,不止是浏览,最好是亲手去实践下.
概述
js有如下两种数据需要经常遍历
- 数组(Array)
- 对象(Object)
同时又提供了如下8种方法方便我们遍历元素
- for
- while(或do~while)
- forEach
- for in
- $.each
- $(selecter).each
- map
- every
最终我们将分析遍历效率选出最佳遍历选手.
本文将针对如下两种数据进行详细的分析和举栗. 下面举栗中如果不加特殊说明将会用到如下数据.
for
语法: for(初始化; 循环执行条件; 每遍历一个元素后做的事情;){}
for循环只能遍历数组,不能遍历对象. 写for循环时有两点需要注意.
- 其一,为了避免遍历时执行多遍计算数组长度的操作,影响效率,建议在循环开始以变量的形式缓存下数组长度,若在循环内部有可能改变数组长度,请务必慎重处理,避免数组越界.
- JavaScript中并没有类似java的块级作用域,for循环内部定义的变量会直接暴露在外(如 i,循环退出后,i变量将等于数组长度,后续代码将能访问到 i 变量的值),因此建议将for循环置于闭包内. 特别要注意的是: 如果在循环内部,前一个元素的遍历有可能影响到后一个元素的遍历,那么for循环内部方法也需要置于闭包之内.
do/while
语法: do{...}while(true);
do/while的语法简化了循环的实现,只保留对循环条件的判断,所以我们要在循环内部构造出循环退出的条件,否则有可能造成死循环. 特别要注意的是: 使用 continue 跳出本次遍历时,要保证循环能够自动进入到下一次遍历,因此保证循环走到下一次遍历的语句需要放到 continue 前面执行,建议置于循环头部.(如上,i++ 语句最好放置循环头部)
do/while 循环与for循环大体差不多,只支持数组遍历,多用于对循环退出条件不是很明确的场景. 一般来说不建议使用这种方式遍历数组.
forEach
语法: array.forEach(function(item){})
,参数item表示数组每一项的元素
forEach回调function默认有三个参数: item,index,array.
使用forEach循环有几点需要特别注意:
- forEach无法遍历对象
- forEach无法在IE中使用,只是在firefox和chrome中实现了该方法
- forEach无法使用break,continue跳出循环,使用return时,效果和在for循环中使用continue一致
for in
语法: for(var item in array){}
for in 可用于遍历数组和对象,但它输出的只是数组的索引和对象的key,我们可以通过索引和key取到对应的值. 如下:
$.each
语法: $.each(array|o,function(i,ele){})
支持数组和对象
这里我们注意到 this对象 指向当前属性的值,这是因为:
参考jQuery api:
$.each() 方法会迭代jQuery对象中的每一个DOM元素。每次回调函数执行时,会传递当前循环次数作为参数(从0开始计数)。更重要的是,回调函数是在当前DOM元素为上下文的语境中触发的。因此关键字 this 总是指向这个元素。 同时,上述遍历时,o 对象的属性中有一个length属性并没有被输出. 这是为什么呢? 请耐心往下看.
首先,我们来看看遍历对象o时,当前的this对象到底是什么?
我们发现,this对象等于回调函数的第二个形参. 且它的 length 属性和 [[PrimitiveValue]] 属性并没有被打印出来,为此我们来查看下length的内部属性.
可见,this对象的length属性的 enumerable 属性被设置成了false,这表示该对象不能被列举或遍历,同时还不能被配置(configurable: false)
,也不能被赋值(writable: false
) .
此时,前面遍历 o 对象时,它的 length 属性没有被打印出来的疑问似乎有解了. 让我们来看看 o.length
的内部属性吧.
o.length
值为3,可赋值,可列举,可配置. 这可不对,刚刚不是说 enumerable 属性被设置成了false 才不会被遍历吗. 现在该值为 true,并且还不可遍历. 这不合常理,自然该有别的原因. 我们接着往下看.
$.each(o,ele);
});
// 0 linda
// 1 style
// 2 nick
// length 5
var o = {0:"linda",2:"nick"}; // 试试去掉length属性
$.each(o,ele);
});
// 0 linda
// 1 style
// 2 nick
现象明了,结合jquery源码,当对象中存在length属性时,$.each
内部使用for循环去遍历对象,否则它将使用for in循环去遍历,因此$.each
遍历对象遵循如下规律:
不仅如此,$.each
的具体使用过程中还有以下几点需要注意:
- 使用 return 或者 return true 为跳过一个元素,继续执行后面的循环;
- 使用 return false 为终止循环的执行,这是因为在
jquery.each
中,若返回值指定为false,才跳出循环,如果感兴趣请翻看jquery.each
源码; - 无法使用 break 与 continue 来跳过循环.
$(selecter).each
语法: $(selecter|array|o).each(function(i,ele){})
支持数组和对象,该方法基本上与$.each
方法相同.
dom表示div元素,由于this恒等ele,说明this也表示div元素,所以this并不是jquery对象,而是普通的DOM对象(可以在this上随意使用DOM方法). 使用$(selecter).each
方法,请注意以下几点:
map
即 Array.prototype.map
,该方法只支持数组
语法: array.map(callback[,thisArg]) map
方法使用其提供函数的每次返回结果生成一个新的数组.
实际上,由于map方法被设计成支持 [鸭式辨型][],该方法也可以用来处理形似数组的对象,例如 NodeList.
甚至还可以用来处理字符串,如下:
map处理字符串的方式多种多样,例如 反转等.
例如 将字符串数组转换为数字数组,只需一条语句,如下:
every
即 Array.prototype.every
,该方法同上述map方法也只支持数组
语法: arr.every(callback[,thisArg]) every
方法用于检验数组中的每一项是否符合某个条件,若符合则放回true,反之则返回false.
该方法还有简写方式,如下:
以上,遍历数组和对象的8种方法简单的介绍完,小结如下:
for in
,$.each
,$().each
既支持对象也支持数组遍历;- for,do/while,forEach 只支持数组;
Array.prototype.map
,Array.prototype.every
只支持数组和形似数组的对象;- forEach不能退出循环,只能通过return来进入到下一个元素的遍历中(相当于for循环的continue),且在IE没有实现该方法;
- $.each和$().each循环只能通过
return false
来退出循环,使用return 或return true
将跳过一个元素,继续执行后面的循环.
测试各方法效率
下面我们来测试下上述方法的效率.
注: array数组默认为空,依次赋值数组长度为1 000 000,10 000 000,100 000 000,分别在 Chrome,Firefox,Safari 浏览器上进行两轮测试,取测试时间平均值作为比较对象,时间单位为ms. 如下是测试代码:
var t1 = +new Date();
var i = 0;
do {
i++;
} while(i<length);
var t2 = +new Date();
console.log('do while:' + (t2-t1));
//-------------------------forEach
var t1 = +new Date();
array.forEach(function(item){
});
var t2 = +new Date();
console.log('forEach:' + (t2-t1));
//-------------------------for in
var t1 = +new Date();
for(var item in array){
}
var t2 = +new Date();
console.log('for in:' + (t2-t1));
//------------------------- $.each
var t1 = +new Date();
$.each(array,ele){
});
var t2 = +new Date();
console.log('$.each:' + (t2-t1));
//-------------------------$().each
var t1 = +new Date();
$(array).each(function(i,ele){
});
var t2 = +new Date();
console.log('$(ele).each:' + (t2-t1));
//-------------------------map
var t1 = +new Date();
array.map(function(num){
});
var t2 = +new Date();
console.log('map:' + (t2-t1));
//-------------------------every
var t1 = +new Date();
array.every(function(e,arr){
});
var t2 = +new Date();
console.log('every:' + (t2-t1));
测试机器正常运行 IDE,编辑器,浏览器,qq,微信等常用应用,系统空闲. 硬件设备如下:
- 操作系统: OSX EI Capitan 版本 10.11.5
- MacBook Pro(13 英寸,2015 年初期)
- 处理器: 2.7 GHz Intel Core i5
- 内存: 8 GB 1867 MHz DDR3
以上多轮测试结果汇总如下三张表(单位:ms):
数组长度为10^6
chrome 52.0.2743.116 (64-bit) | Firefox Developer Edition 49.0a2 (2016-08-01) | Safari 9.1.1 (11601.6.17) |
---|---|---|
数组长度为10^7
chrome 52.0.2743.116 (64-bit) | Firefox Developer Edition 49.0a2 (2016-08-01) | Safari 9.1.1 (11601.6.17) |
---|---|---|
数组长度为10^8
chrome 52.0.2743.116 (64-bit) | Firefox Developer Edition 49.0a2 (2016-08-01) | Safari 9.1.1 (11601.6.17) |
---|---|---|
综上,我们发现for in 循环的性能不稳定,猜测它可能没有进入循环. 因此将数组各元素进行如下赋值. 重新进行如下两轮测试.
数组赋值后,数组长度为10^6
chrome 52.0.2743.116 (64-bit) | Firefox Developer Edition 49.0a2 (2016-08-01) | Safari 9.1.1 (11601.6.17) |
---|---|---|
数组赋值后,数组长度为10^7
chrome 52.0.2743.116 (64-bit) | Firefox Developer Edition 49.0a2 (2016-08-01) | Safari 9.1.1 (11601.6.17) |
---|---|---|
可见,对数组进行赋值后,代码运行基本稳定.(every还不清楚为什么执行时间为0.欢迎大神告知原因.)
分析总结
通过以上 30 次运行测试(实际上为了得到比较稳定的数据,摈弃了许多异常的测试数据),我们发现在数组长度为10^6,10^7,10^8 时,代码运行基本稳定. 各方法运行需要的时间大致排序如下:
我们翻看jquery代码就会知道,$.each方法内部通过调用for循环来实现,而$().each
是先用jquery包裹数组对象,然后再调用for循环,因此后者效率略低于前者.
综上,最佳遍历选手是 for/do while循环,推荐大家优先考虑使用它. ( Firefox浏览器由于对forEach循环做了底层优化,效率接近native,不在我们考虑范围内 ).
基于测试结果的两点思考
从测试数据上猜测,Firefox 与 Safari 似乎对于 for,do while 等都进行了底层优化. 循环执行效率明显优于Chrome.
每次浏览器执行到 for in 循环处,便会出现卡顿,猜测浏览器可能正在预加载循环所需资源(后续我将专门分析此处).
想要进一步优化循环效率,推荐您阅读下篇 《》.
声明: 本文所有数据均为单机测试,难免存在误差,如果发现本文测试数据不对之处,欢迎批评斧正.
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作能带来一定的帮助,如果有疑问大家可以留言交流,谢谢大家对编程之家的支持。
原文链接:https://www.f2er.com/js/39734.html