JavaScript 数组与对象遍历的八种方法及性能深度对比

作者:缓存笔记

时间:2026-10-22

浏览:459

本文深入对比了 JavaScript 中遍历数组和对象的八种常用方法,包括 for、while、forEach、for-in、$.each 等。通过在不同规模数组(10^6 至 10^8)下的 Chrome、Firefox 和 Safari 浏览器实测,揭示了各方法的执行效率差异。结论指出原生 for 和 do-while 循环在大多数浏览器中性能最佳,而 jQuery 封装方法因额外开销效率略低,同时探讨了各方法的兼容性与适用场景。

文章目录

JavaScript 遍历方法概述

前言

遍历数组或对象是程序员的基本功之一。然而,选择合适的遍历方式并非易事,高效的开发者懂得如何挑选合适的方法以提升遍历效率。本文将深入探讨 JavaScript 的遍历机制,通过代码实践与分析,帮助读者掌握循环优化的技巧。文中所有示例代码均可直接运行,建议读者通读后亲手实践,以获得更深刻的理解。

概述

在 JavaScript 开发中,我们通常需要遍历两类数据结构:

  • 数组 (Array)
  • 对象 (Object)

同时提供了如下8种方法方便我们遍历元素

  • for
  • while (或 do-while)
  • forEach
  • for-in
  • $.each
  • $(selector).each
  • map
  • every

本文将深入分析上述方法的执行效率,并选出性能最优的遍历方案。

本文将针对数组和对象两种数据类型进行详细分析与举例。以下示例中,除非特别说明,否则均使用如下测试数据:


var array = ["囚徒","过客","领袖"];//职场3种人
var o = {0:"linda",1:"style",2:"nick",length:3};

原生循环方法详解

for 循环

语法:for(初始化; 循环执行条件; 每遍历一个元素后做的事情;){}


(function(){//循环置于闭包之内
 for(var i=0,length=array.length;i<length;i++){//缓存数组长度
 console.log(array[i]);//内部方法若有可能相互影响,也要置于闭包之内
 }
})();

for 循环仅支持数组遍历,无法直接用于对象。使用 for 循环时需注意两点:

  • 首先,为避免在每次循环中重复计算数组长度从而影响效率,建议在循环开始前将数组长度缓存为变量。若循环内部可能修改数组长度,需格外谨慎处理,以防数组越界。
  • 其次,JavaScript 没有类似 Java 的块级作用域,for 循环内部定义的变量(如索引 i)会暴露到外部(循环结束后 i 等于数组长度,后续代码仍可访问)。因此,建议将 for 循环置于闭包中。特别需要注意的是,如果前一个元素的遍历可能影响后续元素,那么循环内部的方法也需置于闭包内。

do-while 循环

语法:do{...}while(true);


 do while
(function() {
 var i = 0,
 len = array.length;
 do {
 if (i == 2) {
 break; // 循环被终止, 此处如果是continue就会造成循环无法退出
 };
 console.log('array['+ i +']:' + array[i]);
 i++;//此句建议放置循环while头部
 } while(i<len);
})();

do-while 语法简化了循环实现,仅保留对循环条件的判断,因此必须在循环内部构造退出条件,否则可能导致死循环。使用 continue 跳出本次遍历时,需确保循环能自动进入下一次迭代,因此推进循环的语句(如 i++)应置于 continue 之前,建议放在循环头部。

do-while 循环与 for 循环类似,仅支持数组遍历,多用于退出条件不明确的情景。一般不建议使用这种方式遍历数组。

数组迭代方法:forEach、map、every

forEach 方法

语法:array.forEach(function(item){}),参数 item 表示数组每一项的元素


array.forEach(function(item){
 if(item=="囚徒")
 return;//这里只能使用return跳过当前元素处理
 console.log(item);
});

forEach 回调函数默认包含三个参数:item(当前元素)、index(索引)和 array(原数组)。

使用 forEach 循环时需注意以下几点:

  • forEach 无法遍历对象。
  • forEach 在 IE 浏览器中未实现,仅在 Firefox 和 Chrome 等现代浏览器中可用。
  • forEach 不支持使用 break 或 continue 跳出循环;使用 return 时,其效果等同于 for 循环中的 continue。

for-in 循环

语法:for(var item in array){}


for(var item in array){
 console.log(item);
}//0 1 2
for(var item in o){
 console.log(item);
}//0 1 2 length

for-in 可用于遍历数组和对象,但它输出的是数组的索引或对象的键(key)。我们可以通过索引或键获取对应的值。示例如下:


for(var item in array){
 console.log(array[item]);
}//"囚徒" "过客" "领袖"
for(var item in o){
 console.log(o[item]);
}//"linda" "style" "nick" "length"

jQuery 遍历方法:$.each 与 $().each

$.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 对象指向当前属性的值,原因在于:

参考 jQuery API 文档:

$.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,并且还不可遍历。这不合常理,自然该有别的原因。我们接着往下看。


var o = {0:"linda",1:"style",2:"nick",length:1}; // 试着改变length的值
$.each(o, function(i, ele){//再遍历一次
 console.log(i,ele);
});
//0 "linda"

var o = {0:"linda",1:"style",2:"nick",length:5}; // 坚持改变length的值
$.each(o, function(i, ele){//再遍历一次
 console.log(i,ele);
});
// 0 linda
// 1 style
// 2 nick
// length 5

var o = {0:"linda",1:"style",2:"nick"}; // 试试去掉length属性
$.each(o, function(i, ele){//再遍历一次
 console.log(i,ele);
});
// 0 linda
// 1 style
// 2 nick

现象明了,结合 jQuery 源码,当对象中存在 length 属性时,$.each 内部使用 for 循环去遍历对象,否则它将使用 for-in 循环去遍历,因此$.each遍历对象遵循如下规律:

  • 如果对象中存在 length 属性,遍历深度以 length 属性为准,即 length 多大,遍历多少个元素。
  • 如果对象中不存在 length 属性,遍历深度以实际内部属性个数为准。

不仅如此,$.each的具体使用过程中还有以下几点需要注意:

  • 使用 return 或者 return true 为跳过一个元素,继续执行后面的循环;
  • 使用 return false 为终止循环的执行,这是因为在 jquery.each 中,若返回值指定为 false,才跳出循环,如果感兴趣请翻看 jquery.each 源码;
  • 无法使用 break 与 continue 来跳过循环。

$(selector).each 方法

语法:$(selecter|array|o).each(function(i, ele){}),支持数组和对象,该方法基本上与$.each方法相同。


$('div').each(function(i,ele){
 console.log(this,i,this == ele);
});
//dom... 0 dom.... true
$(array).each(function(i,ele){//处理数组
 if(this == "领袖")
 console.log(this,i,this == ele);
});
//String {0: "领", 1: "袖", length: 2, [[PrimitiveValue]]: "领袖"} 2 true
$(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"} 2 true

dom 表示 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,该方法使用其提供函数的每次返回结果生成一个新的数组。


var array = [1, 4, 9];
var roots = array.map(Math.sqrt);//map包裹方法名
// roots is now [1, 2, 3], array is still [1, 4, 9]
var array = [1, 4, 9];
var doubles = array.map(function(num) {//map包裹方法实体
 return num * 2;
});
// doubles is now [2, 8, 18]. array is still [1, 4, 9]

实际上,由于 map 方法被设计成支持 [鸭式辨型],该方法也可以用来处理形似数组的对象,例如 NodeList。


var elems = document.querySelectorAll('select option:checked');
var values = Array.prototype.map.call(elems, function(obj) {
 return obj.value;
});

甚至还可以用来处理字符串,如下:


var map = Array.prototype.map;
var array = map.call('Hello 中国', function(x) { 
 return x.charCodeAt(0);
});
console.log(array);
//[72, 101, 108, 108, 111, 32, 20013, 22269]

map 处理字符串的方式多种多样,例如反转等。


var str = '12345';
var output = Array.prototype.map.call(str, function(x) {
 return x;
}).reverse().join('');
console.log(output);//54321

例如将字符串数组转换为数字数组,只需一条语句,如下:


console.log(['1', '2', '3'].map(Number));//[1,2,3]

目前 map 方法被大部分浏览器支持,除了 IE 6、7、8。

every 方法

即 Array.prototype.every,该方法同上述 map 方法也只支持数组

语法:arr.every(callback[, thisArg]) every ,该方法用于检验数组中的每一项是否符合某个条件,若符合则返回 true,反之则返回 false。


function isBigEnough(element, index, array) {
 return element >= 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 种方法简单的介绍完,小结如下:

  • 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 array = [],
 length = array.length = 10000000;//(一千万)
//for(var i=0;i<length;i++){
// array[i] = 'louis';
//}
console.log(array[0]);
//-------------------------for
var t1 = +new Date();
for(var i=0;i<length;i++){
}
var t2 = +new Date();
console.log('for:' + (t2-t1));

//-------------------------do/while
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, function(i, 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,i,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


数组长度为10^6 chrome 52.0.2743.116 (64-bit) Firefox Developer Edition 49.0a2 (2016-08-01) Safari 9.1.1 (11601.6.17)
for (16+19)/2 = 17.5 (6+7)/2 = 6.5 (6+7)/2 = 6.5
do while (24+17)/2 = 20.5 (7+5)/2 = 6 (5+5)/2 = 5
for in (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 chrome 52.0.2743.116 (64-bit) Firefox Developer Edition 49.0a2 (2016-08-01) Safari 9.1.1 (11601.6.17)
for (164+161)/2 = 162.5 (26+30)/2 = 28 (30+31)/2 = 30.5
do while (163+157)/2 = 160 (27+25)/2 = 26 (28+27)/2 = 27.5
for in (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 chrome 52.0.2743.116 (64-bit) Firefox Developer Edition 49.0a2 (2016-08-01) Safari 9.1.1 (11601.6.17)
for (1486+1583)/2 = 1534.5 (222+238)/2 = 230 (261+251)/2 = 256
do while (1548+1608)/2 = 1578 (236+247)/2 = 241.5 (272+265)/2 = 268.5
for in (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

综上,我们发现 for-in 循环的性能不稳定,猜测它可能没有进入循环。因此将数组各元素进行如下赋值,重新进行如下两轮测试。


var array = [],
 length = array.length = 1000000;
for(var i=0;i<length;i++){
 array[i] = 'louis';
}

数组赋值后,数组长度为 10^6


数组长度为10^6 chrome 52.0.2743.116 (64-bit) Firefox Developer Edition 49.0a2 (2016-08-01) Safari 9.1.1 (11601.6.17)
for (21+22)/2 = 21.5 (8+10)/2 = 9 (6+5)/2 = 5.5
do while (22+19)/2 = 20.5 (6+6)/2 = 6 (6+5)/2 = 5.5
for in (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 chrome 52.0.2743.116 (64-bit) Firefox Developer Edition 49.0a2 (2016-08-01) Safari 9.1.1 (11601.6.17)
for (171+157)/2 = 164 (27+26)/2 = 26.5 (26+28)/2 = 27
do while (168+158)/2 = 163 (27+27)/2 = 27 (28+29)/2 = 28.5
for in (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 ~= do while < forEach ~= map ~= every < $.each < $(e).each < for in

根据统计数据,可得这 8 个方法的运行速度大致排序为:

  1. for 与 do-while
  2. forEach、map、every(这 3 个不相上下,可认为运行速度差不多)
  3. $.each
  4. $(e).each
  5. for-in

我们翻看 jQuery 代码就会知道,$.each 方法内部通过调用 for 循环来实现,而$().each是先用 jQuery 包裹数组对象,然后再调用 for 循环,因此后者效率略低于前者。

综上,最佳遍历选手是 for/do-while 循环,推荐大家优先考虑使用它。(Firefox 浏览器由于对 forEach 循环做了底层优化,效率接近原生,不在我们考虑范围内)。

基于测试结果的两点思考

从测试数据上猜测,Firefox 与 Safari 似乎对于 for、do-while 等都进行了底层优化,循环执行效率明显优于 Chrome。

每次浏览器执行到 for-in 循环处,便会出现卡顿,猜测浏览器可能正在预加载循环所需资源(后续我将专门分析此处)。

想要进一步优化循环效率,推荐您阅读下篇《JS 作用域链及闭包》。

声明:本文所有数据均为单机测试,难免存在误差,如果发现本文测试数据不对之处,欢迎批评斧正。

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作能带来一定的帮助,如果有疑问大家可以留言交流,谢谢大家对本站的支持。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章