前言
前端开发中经常涉及到数组的相关操作: 去重, 过滤, 求和, 数据二次处理等等. 都需要我们对数组进行循环. 为了满足各种需求, 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 的值不再是 https://developer.mozilla.org/en-US/docs/Glossary/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 https://developer.mozilla.org/en-US/docs/web/API/Console/time 和 console.timeEnd https://developer.mozilla.org/en-US/docs/Web/API/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 新增的诸多数组的方法确实极大的方便了前端开发, 使得以往复杂或者冗长的代码, 可以变得易读而且精炼. 如何你对其他数组循环方法的效率也感兴趣, 不妨自己动手试一试, 也欢迎评论交流.
来源: https://juejin.im/post/5b645f536fb9a04fc9376882