JS数组循环的性能效率分析(for、forEach、for of)

前言

前端开发中经常涉及到数组的相关操作:去重、过滤、求和、数据二次处理等等。都需要我们对数组进行循环。为了满足各种需求,JS除了提供最简单的for循环,在ES6和后续版本中也新增的诸如:map、filter、some、reduce等实用的方法。因为各个方法作用不同,简单的对所有涉及到循环的方法进行单纯执行速度比较,是不公平的,也是毫无意义的。那么我们就针对最单纯的以取值为目的的循环进行一次效率测试,用肉眼可见的方式,探讨一下JS中这些数组循环方式的效率。

从最简单的for循环说起

for循环常见的有三种写法,不啰嗦,直接上代码

const persons = ['郑昊川', '钟忠', '高晓波', '韦贵铁', '杨俊', '宋灿']
// 方法一
for (let i = 0; i < persons.length; i++) {
  console.log(persons[i])
}
// 方法二
for (let i = 0, len = persons.length; i < len; i++) {
  console.log(persons[i])
}
// 方法三
for (let i = 0; person = persons[i]; i++) {
  console.log(person)
}
复制代码

第一种方法是最常见的方式,不解释。

第二种方法是将persons.length缓存到变量len中,这样每次循环时就不会再对数组的长度进行运算。

第三种方式的执行顺序是:

  • 第一步:先声明索引i = 0
  • 第二步:取出数组中当前索引对应的值persons[i]并赋值给person变量(和函数设置参数默认值的行为不同,这里的person是全局变量,谨慎使用)
  • 第三步:执行循环体,打印person
  • 第四步:i++

当第二步的person的值不再是Truthy时,循环结束。方法三甚至可以这样写

for (let i = 0; person = persons[i++];) {
  console.log(person)
}
复制代码

三种for循环方式在数组浅拷贝中的速度测试

先造一个足够长的数组作为要拷贝的目标(如果i值过大,到千万级,可能会抛出JS堆栈跟踪的报错)

var hugeArr = []
var i = 6666666
while (i > 0) {
  hugeArr.push(i)
  i--
}
复制代码

然后分别用三种循环方式,把数组中的每一项取出,并添加到一个空数组中,也就是一次数组的浅拷贝。并通过console.timeconsole.timeEnd记录每种循环方式的整体执行时间。

// 方法一
function method1() {
  var arrCopy = []
  console.time('method1')
  for (let i = 0; i < hugeArr.length; i++) {
    arrCopy.push(hugeArr[i])
  }
  console.timeEnd('method1')
}
// 方法二
function method2() {
  var arrCopy = []
  console.time('method2')
  for (let i = 0, len = hugeArr.length; i < len; i++) {
    arrCopy.push(hugeArr[i])
  }
  console.timeEnd('method2')
}
// 方法三
function method3() {
  var arrCopy = []
  console.time('method3')
  for (let i = 0; item = hugeArr[i]; i++) {
    arrCopy.push(item)
  }
  console.timeEnd('method3')
}
复制代码

分别调用上述方法,每个方法重复执行12次,去除一个最大值和一个最小值,求平均值,最终每个方法执行时间的结果如下表(测试机器:MacBook Pro (15-inch, 2017) 处理器:2.8 GHz Intel Core i7 内存:16 GB 2133 MHz LPDDR3):

次数 方法一 方法二 方法三
第一次 166.65087890625ms 169.19482421875ms 180.170166015625ms
第二次 170.634033203125ms 165.016845703125ms 182.826171875ms
第三次 170.75927734375ms 169.642822265625ms 183.2890625ms
第四次 171.494873046875ms 172.0009765625ms 178.19091796875ms
第五次 166.44189453125ms 177.200927734375ms 179.85986328125ms
第六次 173.19287109375ms 167.947021484375ms 182.949951171875ms
第七次 166.638916015625ms 171.447021484375ms 181.72509765625ms
第八次 167.666259765625ms 176.8828125ms 182.670166015625ms
第九次 170.364013671875ms 168.118896484375ms 182.511962890625ms
第十次 166.06689453125ms 173.218017578125ms 179.755126953125ms
平均值 168.9909912109375 171.0670166015625ms 181.3948486328125ms

意不意外?惊不惊喜?想象之中应该是方法二最快呀!但事实并非如此,不相信眼前事实的我又测试了很多次,包括改变被拷贝的数组的长度,长度从百级到千万级。最后得出的结论是完成同一个数组的浅拷贝任务耗时方法一 < 方法二 < 方法三。至于为什么会这样,个人感觉JS在执行hugeArr.length这个取值操作时,即使我们没有把它赋给一个变量,可能hugeArr.length也已经缓存下来了,反倒是方法二一开始执行len = hugeArr.length,相当于多了一步赋值操作,所以我们在声明len变量来存储数组长度是没有多大意义的。当然这只是我个人的猜想,如果各位大佬有更合理,更科学的解释,欢迎在评论区不吝赐教。回到大量类似数组浅拷贝的实际应用场景下,第一种最常用也是最简单的for循环方式确实是效率最高的,个人不建议大家使用第三种方式,因为如果数组里存在非Truthy的值,比如0'',会导致循环直接结束。

forEachfor of 这些ES6语法,会更快吗?

实践是检验真理的唯一标准

// 方法四
function method4() {
  var arrCopy = []
  console.time('method4')
  hugeArr.forEach((item) => {
    arrCopy.push(item)
  })
  console.timeEnd('method4')
}
// 方法五
function method5() {
  var arrCopy = []
  console.time('method5')
  for (let item of hugeArr) {
    arrCopy.push(item)
  }
  console.timeEnd('method5')
}
复制代码

测试方法同上,测试结果:

次数 方法四 方法五
第一次 251.239990234375ms 234.830078125ms
第二次 249.3427734375ms 258.794189453125ms
第三次 245.44384765625ms 237.998046875ms
第四次 249.087890625ms 263.808837890625ms
第五次 247.385986328125ms 232.47900390625ms
第六次 245.661865234375ms 258.749755859375ms
第七次 242.88623046875ms 226.119873046875ms
第八次 244.367919921875ms 255.99609375ms
第九次 248.890869140625ms 226.197021484375ms
第十次 254.41992187ms 251.48583984375ms
平均值 247.8727294921875ms 244.6458740234375ms

由上面的数据可以很明显的看出,forEachfor of 这种ES6语法虽然在使用中会带来很多便利,但是单纯从执行速度上看,并没有传统的for循环快。而且for循环是可以通过break关键字跳出的,而forEach这种循环是无法跳出的。

总结

之前有听到过一些类似“缓存数组长度提高循环速度”或者“ES6的循环语法更高效”的说法。说者无心,听者有意,事实究竟如何,实践出真知。但是ES6新增的诸多数组的方法确实极大的方便了前端开发,使得以往复杂或者冗长的代码,可以变得易读而且精炼。如何你对其他数组循环方法的效率也感兴趣,不妨自己动手试一试,也欢迎评论交流。

猜你喜欢

转载自juejin.im/post/5b645f536fb9a04fc9376882