前言
前端开发中经常涉及到数组的相关操作:去重、过滤、求和、数据二次处理等等。都需要我们对数组进行循环。为了满足各种需求,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.time和console.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
和''
,会导致循环直接结束。
forEach
和for 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 |
由上面的数据可以很明显的看出,forEach
和for of
这种ES6
语法虽然在使用中会带来很多便利,但是单纯从执行速度上看,并没有传统的for
循环快。而且for循环
是可以通过break
关键字跳出的,而forEach
这种循环是无法跳出的。
总结
之前有听到过一些类似“缓存数组长度提高循环速度”或者“ES6的循环语法更高效”的说法。说者无心,听者有意,事实究竟如何,实践出真知。但是ES6新增的诸多数组的方法确实极大的方便了前端开发,使得以往复杂或者冗长的代码,可以变得易读而且精炼。如何你对其他数组循环方法的效率也感兴趣,不妨自己动手试一试,也欢迎评论交流。