本文永久连接:github.com/HaoChuan942…html
前端开发中常常涉及到数组的相关操做:去重、过滤、求和、数据二次处理等等。都须要咱们对数组进行循环。为了知足各类需求,JS除了提供最简单的for
循环,在ES6
和后续版本中也新增的诸如:map、filter、some、reduce
等实用的方法。由于各个方法做用不一样,简单的对全部涉及到循环的方法进行执行速度比较,是不公平的,也是毫无心义的。那么咱们就针对最单纯的以取值为目的的循环进行一次性能和效率测试,用肉眼可见的方式,对JS中常见的这些数组循环方式进行一次探讨。前端
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; person = persons[i]; i++) {
console.log(person)
}
// 方法四
for (let i = persons.length; i--;) {
console.log(persons[i])
}
复制代码
第一种方法是最多见的方式,不解释。node
第二种方法是将persons.length
缓存到变量len
中,这样每次循环时就不会再读取数组的长度。git
第三种方式是将取值与判断合并,经过不停的枚举每一项来循环,直到枚举到空值则循环结束。执行顺序是:github
第四种方法是倒序循环。执行的顺序是:浏览器
i
i
是否大于0并执行i--
persons[i]
,此时的i
已经-1
了
从后向前,直到
i === 0
为止。这种方式不只去除了每次循环中读取数组长度的操做,并且只建立了一个变量i
。缓存
for
循环方式在数组浅拷贝中的性能和速度测试先造一个足够长的数组做为要拷贝的目标(若是i
值过大,到亿级左右,可能会抛出JS堆栈跟踪的报错)性能优化
const ARR_SIZE = 6666666
const hugeArr = new Array(ARR_SIZE).fill(1)
复制代码
而后分别用四种循环方式,把数组中的每一项取出,并添加到一个空数组中,也就是一次数组的浅拷贝。并经过console.time和console.timeEnd记录每种循环方式的总体执行时间。经过process.memoryUsage()比对执行先后内存中已用到的堆的差值。post
/* node环境下记录方法执行先后内存中已用到的堆的差值 */
function heapRecord(fun) {
if (process) {
const startHeap = process.memoryUsage().heapUsed
fun()
const endHeap = process.memoryUsage().heapUsed
const heapDiff = endHeap - startHeap
console.log('已用到的堆的差值: ', heapDiff)
} else {
fun()
}
}
复制代码
// 方法一,普通for循环
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; item = hugeArr[i]; i++) {
arrCopy.push(item)
}
console.timeEnd('method3')
}
// 方法四,i--与判断合并,倒序迭代
function method4() {
var arrCopy = []
console.time('method4')
for (let i = hugeArr.length; i--;) {
arrCopy.push(hugeArr[i])
}
console.timeEnd('method4')
}
复制代码
分别调用上述方法,每一个方法重复执行12次,去除一个最大值和一个最小值,求平均值(四舍五入),最终每一个方法执行时间的结果以下表(测试机器:MacBook Pro (15-inch, 2017) 处理器:2.8 GHz Intel Core i7 内存:16 GB 2133 MHz LPDDR3
执行环境:node v10.8.0
):
- | 方法一 | 方法二 | 方法三 | 方法四 |
---|---|---|---|---|
第一次 | 152.201ms | 156.990ms | 152.668ms | 152.684ms |
第二次 | 150.047ms | 159.166ms | 159.333ms | 152.455ms |
第三次 | 155.390ms | 151.823ms | 159.365ms | 149.809ms |
第四次 | 153.195ms | 155.994ms | 155.325ms | 150.562ms |
第五次 | 151.823ms | 154.689ms | 156.483ms | 148.067ms |
第六次 | 152.715ms | 154.677ms | 153.135ms | 150.787ms |
第七次 | 152.084ms | 152.587ms | 157.458ms | 152.572ms |
第八次 | 152.509ms | 153.781ms | 153.277ms | 152.263ms |
第九次 | 154.363ms | 156.497ms | 151.002ms | 154.310ms |
第十次 | 153.784ms | 155.612ms | 161.767ms | 153.487ms |
平均耗时 | 152.811ms | 155.182ms | 155.981ms | 151.700ms |
用栈差值 | 238511136Byte | 238511352Byte | 238512048Byte | 238511312Byte |
意不意外?惊不惊喜?想象之中至少方法二确定比方法一更快的!但事实并不是如此,不相信眼前事实的我又测试了不少次,包括改变被拷贝的数组的长度,长度从百级到千万级。最后发现:在node
下执行完成同一个数组的浅拷贝任务,耗时方面四种方法的差距微乎其微,有时候排序甚至略有波动。 内存占用方面:方法一 < 方法四 < 方法二 < 方法三,但差距也很小。
v8引擎
新版本针对对象取值等操做进行了最大限度的性能优化,因此方法二中缓存数组的长度到变量len
中,并不会有太明显的提高。即便是百万级的数据,四种for循环
的耗时差距也只是毫秒级,内存占用上四种for循环方式也都很是接近。在此感谢YaHuiLiang、七秒先生、戈寻谋doxP、超级大柱子的帮助和指正,若是大佬们有更好的看法也欢迎评论留言。
一样是v8引擎
的谷歌浏览器
,测试发现四种方法也都很是接近。
可是在火狐浏览器
中的测试结果:方法二 ≈ 方法三 ≈ 方法四 < 方法一,代表二三四这三种写法均可以在必定程度上优化for循环
而在safari浏览器下
:方法四 < 方法一 ≈ 方法二 ≈ 方法三,只有方法四体现出了小幅度的优化效果。
考虑到在不一样环境或浏览器下的性能和效率:
推荐
:第四种i--
倒序循环的方式。在奇舞团的这篇文章——嗨,送你一张Web性能优化地图的2.3 流程控制
小节里也略有说起这种方式。
不推荐
:第三种方式。主要是由于当数组里存在非Truthy
的值时,好比0
和''
,会致使循环直接结束。
while
循环以及ES6+的新语法forEach
、map
和for of
,会更快吗?不啰嗦,实践是检验真理的惟一标准
// 方法五,while
function method5() {
var arrCopy = []
console.time('method5')
let i = 0
while (i < hugeArr.length) {
arrCopy.push(hugeArr[i++])
}
console.timeEnd('method5')
}
// 方法六,forEach
function method6() {
var arrCopy = []
console.time('method6')
hugeArr.forEach((item) => {
arrCopy.push(item)
})
console.timeEnd('method6')
}
// 方法七,map
function method7() {
var arrCopy = []
console.time('method7')
arrCopy = hugeArr.map(item => item)
console.timeEnd('method7')
}
// 方法八,for of
function method8() {
var arrCopy = []
console.time('method8')
for (let item of hugeArr) {
arrCopy.push(item)
}
console.timeEnd('method8')
}
复制代码
测试方法同上,测试结果:
- | 方法五 | 方法六 | 方法七 | 方法八 |
---|---|---|---|---|
第一次 | 151.380ms | 221.332ms | 875.402ms | 240.411ms |
第二次 | 152.031ms | 223.436ms | 877.112ms | 237.208ms |
第三次 | 150.442ms | 221.853ms | 876.829ms | 253.744ms |
第四次 | 151.319ms | 222.672ms | 875.270ms | 243.165ms |
第五次 | 150.142ms | 222.953ms | 877.940ms | 237.825ms |
第六次 | 155.226ms | 225.441ms | 879.223ms | 240.648ms |
第七次 | 151.254ms | 219.965ms | 883.324ms | 238.197ms |
第八次 | 151.632ms | 218.274ms | 878.331ms | 240.940ms |
第九次 | 151.412ms | 223.189ms | 873.318ms | 256.644ms |
第十次 | 155.563ms | 220.595ms | 881.203ms | 234.534ms |
平均耗时 | 152.040ms | 221.971ms | 877.795ms | 242.332ms |
用栈差值 | 238511400Byte | 238511352Byte | 53887824Byte | 191345296Byte |
在node
下,由上面的数据能够很明显的看出,forEach
、map
和for of
这些ES6+
的语法并无传统的for
循环或者while
循环快,特别是map
方法。可是因为map
有返回值,无需额外调用新数组的push
方法,因此在执行浅拷贝任务上,内存占用很低。而for of
语法在内存占用上也有必定的优点。顺便提一下:for循环 while循环 for of 循环
是能够经过break
关键字跳出的,而forEach map
这种循环是没法跳出的。
可是随着执行环境和浏览器的不一样,这些语法在执行速度上也会出现误差甚至反转的状况,直接看图:
谷歌浏览器
火狐浏览器
safari浏览器下
能够看出:
ES6+
的循环语法会广泛比传统的循环语法慢,可是火狐和safari中状况却几乎相反。map
特殊,速度明显比其余几种语法慢,而在火狐和safari中却出现了反转,map
反而比较快!以前有听到过诸如“缓存数组长度能够提升循环效率”或者“ES6的循环语法更高效”的说法。说者无意,听者有意,事实究竟如何,实践出真知。抛开业务场景和使用便利性,单纯谈性能和效率是没有意义的。 ES6新增的诸多数组的方法确实极大的方便了前端开发,使得以往复杂或者冗长的代码,能够变得易读并且精炼,而好的for
循环写法,在大数据量的状况下,确实也有着更好的兼容和多环境运行表现。固然本文的讨论也只是基于观察的一种总结,并无深刻底层。而随着浏览器的更新,这些方法的孰优孰劣也可能成为玄学。目前发如今Chrome Canary 70.0.3513.0
下for of
会明显比Chrome 68.0.3440.84
快。若是你有更深刻的看法或者文章,也不妨在评论区分享,小弟的这篇文章也权当抛砖引玉。若是你对数组的其余循环方法的性能和效率也感兴趣,不妨本身动手试一试,也欢迎评论交流。
本文的测试环境:
node v10.8.0
、Chrome 68.0.3440.84
、Safari 11.1.2 (13605.3.8)
、Firefox 60.0