Javascript中八种遍历方法的执行速度深度对比
前言
遍历数组或对象是一名程序员的基本素养之一.然而遍历却不是一件简单的事,优秀的程序员知道怎么去选择合适的遍历方法,优化遍历效率.本篇将带你走进JavaScript遍历的世界,享受分析JS循环的快感.本篇所有代码都可以直接运行,希望您通读本篇后,不止是浏览,最好是亲手去实践下.
概述
js有如下两种数据需要经常遍历
- 数组(Array)
- 对象(Object)
同时又提供了如下8种方法方便我们遍历元素
- for
- while(或do~while)
- forEach
- forin
- $.each
- $(selecter).each
- map
- every
最终我们将分析遍历效率选出最佳遍历选手.
本文将针对如下两种数据进行详细的分析和举栗.下面举栗中如果不加特殊说明将会用到如下数据.
vararray=["囚徒","过客","领袖"];//职场3种人 varo={0:"linda",1:"style",2:"nick",length:3};
for
语法:for(初始化;循环执行条件;每遍历一个元素后做的事情;){}
(function(){//循环置于闭包之内 for(vari=0,length=array.length;ifor循环只能遍历数组,不能遍历对象.写for循环时有两点需要注意.
- 其一,为了避免遍历时执行多遍计算数组长度的操作,影响效率,建议在循环开始以变量的形式缓存下数组长度,若在循环内部有可能改变数组长度,请务必慎重处理,避免数组越界.
- JavaScript中并没有类似java的块级作用域,for循环内部定义的变量会直接暴露在外(如i,循环退出后,i变量将等于数组长度,后续代码将能访问到i变量的值),因此建议将for循环置于闭包内.特别要注意的是:如果在循环内部,前一个元素的遍历有可能影响到后一个元素的遍历,那么for循环内部方法也需要置于闭包之内.
do/while
语法:do{...}while(true);
dowhile (function(){ vari=0, len=array.length; do{ if(i==2){ break;//循环被终止,此处如果是continue就会造成循环无法退出 }; console.log('array['+i+']:'+array[i]); i++;//此句建议放置循环while头部 }while(ido/while的语法简化了循环的实现,只保留对循环条件的判断,所以我们要在循环内部构造出循环退出的条件,否则有可能造成死循环.特别要注意的是:使用continue跳出本次遍历时,要保证循环能够自动进入到下一次遍历,因此保证循环走到下一次遍历的语句需要放到continue前面执行,建议置于循环头部.(如上,i++语句最好放置循环头部)
do/while循环与for循环大体差不多,只支持数组遍历,多用于对循环退出条件不是很明确的场景.一般来说不建议使用这种方式遍历数组.
forEach
语法:array.forEach(function(item){}),参数item表示数组每一项的元素
array.forEach(function(item){ if(item=="囚徒") return;//这里只能使用return跳过当前元素处理 console.log(item); });forEach回调function默认有三个参数:item,index,array.
使用forEach循环有几点需要特别注意:
- forEach无法遍历对象
- forEach无法在IE中使用,只是在firefox和chrome中实现了该方法
- forEach无法使用break,continue跳出循环,使用return时,效果和在for循环中使用continue一致
forin
语法:for(variteminarray){}
for(variteminarray){ console.log(item); }//012 for(varitemino){ console.log(item); }//012lengthforin可用于遍历数组和对象,但它输出的只是数组的索引和对象的key,我们可以通过索引和key取到对应的值.如下:
for(variteminarray){ console.log(array[item]); }//"囚徒""过客""领袖" for(varitemino){ console.log(o[item]); }//"linda""style""nick""length"$.each
语法:$.each(array|o,function(i,ele){})支持数组和对象
$.each(array,function(i,ele){ console.log(i,ele,this==ele); }); //0"囚徒"true //1"过客"true //2"领袖"true $.each(o,function(i,ele){ console.log(i,ele,this==ele); }); //0"linda"true //1"style"true //2"nick"true这里我们注意到this对象指向当前属性的值,这是因为:
参考jQueryapi:
$.each()方法会迭代jQuery对象中的每一个DOM元素。每次回调函数执行时,会传递当前循环次数作为参数(从0开始计数)。更重要的是,回调函数是在当前DOM元素为上下文的语境中触发的。因此关键字this总是指向这个元素。
同时,上述遍历时,o对象的属性中有一个length属性并没有被输出.这是为什么呢?请耐心往下看.首先,我们来看看遍历对象o时,当前的this对象到底是什么?
$.each(o,function(i,ele){ if(this=="linda"){//我们随机选取第一个属性 console.log(this,this==ele); $.each(this,function(e,ele2){ console.log(e,ele2); }); } }); //String{0:"l",1:"i",2:"n",3:"d",4:"a",length:5,[[PrimitiveValue]]:"linda"}true //0"l" //1"i" //2"n" //3"d" //4"a"我们发现,this对象等于回调函数的第二个形参.且它的length属性和[[PrimitiveValue]]属性并没有被打印出来,为此我们来查看下length的内部属性.
$.each(o,function(i,ele){ if(this=="linda")//我们还是随机选取第一个属性(这还是随机吗?) console.log(Object.getOwnPropertyDescriptor(this,'length')); }); //Object{value:5,writable:false,enumerable:false,configurable:false}可见,this对象的length属性的enumerable属性被设置成了false,这表示该对象不能被列举或遍历,同时还不能被配置(configurable:false),也不能被赋值(writable:false).
此时,前面遍历o对象时,它的length属性没有被打印出来的疑问似乎有解了.让我们来看看o.length的内部属性吧.
console.log(Object.getOwnPropertyDescriptor(o,'length')); //Object{value:3,writable:true,enumerable:true,configurable:true}o.length值为3,可赋值,可列举,可配置.这可不对,刚刚不是说enumerable属性被设置成了false才不会被遍历吗.现在该值为true,并且还不可遍历.这不合常理,自然该有别的原因.我们接着往下看.
varo={0:"linda",1:"style",2:"nick",length:1};//试着改变length的值 $.each(o,function(i,ele){//再遍历一次 console.log(i,ele); }); //0"linda" varo={0:"linda",1:"style",2:"nick",length:5};//坚持改变length的值 $.each(o,function(i,ele){//再遍历一次 console.log(i,ele); }); //0linda //1style //2nick //length5 varo={0:"linda",1:"style",2:"nick"};//试试去掉length属性 $.each(o,function(i,ele){//再遍历一次 console.log(i,ele); }); //0linda //1style //2nick现象明了,结合jquery源码,当对象中存在length属性时,$.each内部使用for循环去遍历对象,否则它将使用forin循环去遍历,因此$.each遍历对象遵循如下规律:
- 如果对象中存在length属性,遍历深度以length属性为准,即length多大,遍历多少个元素.
- 如果对象中不存在length属性,遍历深度以实际内部属性个数为准.
不仅如此,$.each的具体使用过程中还有以下几点需要注意:
- 使用return或者returntrue为跳过一个元素,继续执行后面的循环;
- 使用returnfalse为终止循环的执行,这是因为在jquery.each中,若返回值指定为false,才跳出循环,如果感兴趣请翻看jquery.each源码;
- 无法使用break与continue来跳过循环.
$(selecter).each
语法:$(selecter|array|o).each(function(i,ele){})支持数组和对象,该方法基本上与$.each方法相同.
$('div').each(function(i,ele){ console.log(this,i,this==ele); }); //dom...0dom....true $(array).each(function(i,ele){//处理数组 if(this=="领袖") console.log(this,i,this==ele); }); //String{0:"领",1:"袖",length:2,[[PrimitiveValue]]:"领袖"}2true $(o).each(function(i,ele){//处理对象 if(this=="nick") console.log(this,i,this==ele); }); //String{0:"n",1:"i",2:"c",3:"k",length:4,[[PrimitiveValue]]:"nick"}2truedom表示div元素,由于this恒等ele,说明this也表示div元素,所以this并不是jquery对象,而是普通的DOM对象(可以在this上随意使用DOM方法).使用$(selecter).each方法,请注意以下几点:
- i:即序列值ele:表示当前被遍历的DOM元素
- this表示当前被遍历的DOM元素,不能调用jQuery方法,如需调用jquery方法需要用$符号包裹.如,$(this)
map
即Array.prototype.map,该方法只支持数组
语法:array.map(callback[,thisArg])map方法使用其提供函数的每次返回结果生成一个新的数组.
vararray=[1,4,9]; varroots=array.map(Math.sqrt);//map包裹方法名 //rootsisnow[1,2,3],arrayisstill[1,4,9] vararray=[1,4,9]; vardoubles=array.map(function(num){//map包裹方法实体 returnnum*2; }); //doublesisnow[2,8,18].arrayisstill[1,4,9]实际上,由于map方法被设计成支持[鸭式辨型][],该方法也可以用来处理形似数组的对象,例如NodeList.
varelems=document.querySelectorAll('selectoption:checked'); varvalues=Array.prototype.map.call(elems,function(obj){ returnobj.value; });甚至还可以用来处理字符串,如下:
varmap=Array.prototype.map; vararray=map.call('Hello中国',function(x){ returnx.charCodeAt(0); }); console.log(array); //[72,101,108,108,111,32,20013,22269]map处理字符串的方式多种多样,例如反转等.
varstr='12345'; varoutput=Array.prototype.map.call(str,function(x){ returnx; }).reverse().join(''); console.log(output);//54321例如将字符串数组转换为数字数组,只需一条语句,如下:
console.log(['1','2','3'].map(Number));//[1,2,3]目前map方法被大部分浏览器支持,除了IE6,7,8.
every
即Array.prototype.every,该方法同上述map方法也只支持数组
语法:arr.every(callback[,thisArg])every方法用于检验数组中的每一项是否符合某个条件,若符合则放回true,反之则返回false.
functionisBigEnough(element,index,array){ returnelement>=10; } [12,5,8,130,44].every(isBigEnough);//false [12,54,18,130,44].every(isBigEnough);//true该方法还有简写方式,如下:
[12,5,8,130,44].every(elem=>elem>=10);//false [12,54,18,130,44].every(elem=>elem>=10);//true以上,遍历数组和对象的8种方法简单的介绍完,小结如下:
- forin,$.each,$().each既支持对象也支持数组遍历;
- for,do/while,forEach只支持数组;
- Array.prototype.map,Array.prototype.every只支持数组和形似数组的对象;
- forEach不能退出循环,只能通过return来进入到下一个元素的遍历中(相当于for循环的continue),且在IE没有实现该方法;
- $.each和$().each循环只能通过returnfalse来退出循环,使用return或returntrue将跳过一个元素,继续执行后面的循环.
测试各方法效率
下面我们来测试下上述方法的效率.
注:array数组默认为空,依次赋值数组长度为1000000,10000000,100000000,分别在Chrome,Firefox,Safari浏览器上进行两轮测试,取测试时间平均值作为比较对象,时间单位为ms.如下是测试代码:
vararray=[], length=array.length=10000000;//(一千万) //for(vari=0;i测试机器正常运行IDE,编辑器,浏览器,qq,微信等常用应用,系统空闲.硬件设备如下:
- 操作系统:OSXEICapitan版本10.11.5
- MacBookPro(13英寸,2015年初期)
- 处理器:2.7GHzIntelCorei5
- 内存:8GB1867MHzDDR3
以上多轮测试结果汇总如下三张表(单位:ms):
数组长度为10^6
数组长度为10^6 chrome52.0.2743.116(64-bit) FirefoxDeveloperEdition49.0a2(2016-08-01) Safari9.1.1(11601.6.17) for (16+19)/2=17.5 (6+7)/2=6.5 (6+7)/2=6.5 dowhile (24+17)/2=20.5 (7+5)/2=6 (5+5)/2=5 forin (19+28)/2=23.5 (0+0)/2=0 (0+0)/2=0 forEach (41+28)/2=34.5 (4+4)/2=4 (31+29)/2=30 map (26+32)/2=28 (4+4)/2=4 (32+26)/2=28 every (22+24)/2=23 (4+5)/2=4.5 (41+45)/2=43 $.each (29+27)/2=28 (306+311)/2=308.5 (111+97)/2=104 $(e).each (94+98)/2=96 (484+488)/2=486 (79+64)/2=71.5
数组长度为10^7
数组长度为10^7 chrome52.0.2743.116(64-bit) FirefoxDeveloperEdition49.0a2(2016-08-01) Safari9.1.1(11601.6.17) for (164+161)/2=162.5 (26+30)/2=28 (30+31)/2=30.5 dowhile (163+157)/2=160 (27+25)/2=26 (28+27)/2=27.5 forin (78+86)/2=82 (0+0)/2=0 (0+0)/2=0 forEach (211+205)/2=208 (31+30)/2=30.5 (291+289)/2=290 map (349+282)/2=315.5 (24+22)/2=23 (259+260)/2=259.5 every (221+219)/2=220 (24+24)/2=24 (251+257)/2=254 $.each (210+215)/2=212.5 (2868+2789)/2=2828.5 (699+724)/2=711.5 $(e).each (730+669)/2=699.5 (4674+4722)/2=4698 (523+546)/2=534.5
数组长度为10^8
数组长度为10^8 chrome52.0.2743.116(64-bit) FirefoxDeveloperEdition49.0a2(2016-08-01) Safari9.1.1(11601.6.17) for (1486+1583)/2=1534.5 (222+238)/2=230 (261+251)/2=256 dowhile (1548+1608)/2=1578 (236+247)/2=241.5 (272+265)/2=268.5 forin (0+0)/2=0 (0+0)/2=0 (0+0)/2=0 forEach (25838+22307)/2=24072.5 (212+209)/2=210.5 (2565+2568)/2=2566.5 map (23795+22787)/2=23291 (215+206)/2=210.5 (2556+2573)/2=2564.5 every (22393+22378)/2=22385.5 (212+215)/2=213.5 (2550+2548)/2=2549 $.each (14523+14776)/2=14649.5 (28007+27698)/2=27852.5 (7109+7156)/2=7132.5 $(e).each chrome奔溃了... (49352+49530)/2=49441 (5505+4616)/2=5060.5
综上,我们发现forin循环的性能不稳定,猜测它可能没有进入循环.因此将数组各元素进行如下赋值.重新进行如下两轮测试.
vararray=[], length=array.length=1000000; for(vari=0;i数组赋值后,数组长度为10^6
数组长度为10^6 chrome52.0.2743.116(64-bit) FirefoxDeveloperEdition49.0a2(2016-08-01) Safari9.1.1(11601.6.17) for (21+22)/2=21.5 (8+10)/2=9 (6+5)/2=5.5 dowhile (22+19)/2=20.5 (6+6)/2=6 (6+5)/2=5.5 forin (178+184)/2=181 (318+268)/2=293 (413+464)/2=438.5 forEach (42+45)/2=43.5 (4+4)/2=4 (21+24)/2=22.5 map (137+153)/2=145 (9+8)/2=8.5 (38+43)/2=40.5 every (0+0)/2=0 (0+0)/2=0 (0+0)/2=0 $.each (85+84)/2=84.5 (15+19)/2=17 (37+25)/2=31 $(e).each (81+83)/2=82 (34+31)/2=32.5 (37+46)/2=41.5
数组赋值后,数组长度为10^7
数组长度为10^7 chrome52.0.2743.116(64-bit) FirefoxDeveloperEdition49.0a2(2016-08-01) Safari9.1.1(11601.6.17) for (171+157)/2=164 (27+26)/2=26.5 (26+28)/2=27 dowhile (168+158)/2=163 (27+27)/2=27 (28+29)/2=28.5 forin (1469+1715)/2=1592 (2922+3123)/2=3022.5 (5755+5742)/2=5748.5 forEach (347+329)/2=338 (32+36)/2=34 (171+174)/2=172.5 map (1320+1335)/2=1327.5 (147+137)/2=142 (448+469)/2=458.5 every (0+0)/2=0 (0+0)/2=0 (0+0)/2=0 $.each (438+441)/2=439.5 (142+141)/2=141.5 (254+248)/2=251 $(e).each (876+935)/2=905.5 (315+328)/2=321.5 (450+402)/2=426
可见,对数组进行赋值后,代码运行基本稳定.(every还不清楚为什么执行时间为0.欢迎大神告知原因.)
分析总结
通过以上30次运行测试(实际上为了得到比较稳定的数据,摈弃了许多异常的测试数据),我们发现在数组长度为10^6,10^7,10^8时,代码运行基本稳定.各方法运行需要的时间大致排序如下:
for~=dowhile根据统计数据,可得这8个方法的运行速度大致排序为:
- for与dowhile
- forEachmapevery(这3个不相上下,可认为运行速度差不多)
- $.each
- $(e).each
- forin
我们翻看jquery代码就会知道,$.each方法内部通过调用for循环来实现,而$().each是先用jquery包裹数组对象,然后再调用for循环,因此后者效率略低于前者.
综上,最佳遍历选手是for/dowhile循环,推荐大家优先考虑使用它.(Firefox浏览器由于对forEach循环做了底层优化,效率接近native,不在我们考虑范围内).
基于测试结果的两点思考
从测试数据上猜测,Firefox与Safari似乎对于for,dowhile等都进行了底层优化.循环执行效率明显优于Chrome.
每次浏览器执行到forin循环处,便会出现卡顿,猜测浏览器可能正在预加载循环所需资源(后续我将专门分析此处).
想要进一步优化循环效率,推荐您阅读下篇《JS作用域链及闭包》.
声明:本文所有数据均为单机测试,难免存在误差,如果发现本文测试数据不对之处,欢迎批评斧正.
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作能带来一定的帮助,如果有疑问大家可以留言交流,谢谢大家对毛票票的支持。