前言
在做项目开发的过程中,看到一处代码使用原始的
for循环代替forEach进行遍历,代码注释中解释此举是为了获得更好的运行效率。好奇这两种遍历方式的差别,遂找到这篇文章。原版为英文,这里进行机翻 + 人工修正,望能让大家对迭代器遍历和循环遍历多一点了解。
有一次,我们面试了一位中级开发工程师,他连一个涉及 while 循环的简单问题都回答不出来。他解释说,他只写声明性代码,命令式编程已经没有意义了。虽然我们部分同意,但这也让我们思考:程序员应该总是倾向于使用 .map, .reduce 和 .forEach 数组方法,而不考虑使用 JavaScript 的循环语句吗?
“声明式编程风格非常具有表现力,易于编写,可读性也强得多。”这个观点在 99% 的情况下都是说得通的,但是在更考虑性能的时候不是这样,循环语句通常比声明式的语句效率快三倍以上。在大多数应用程序中,这并不是一个显著的差异,但当在一些商业分析应用、视频处理、科学计算或游戏引擎中处理大量数据时,效率的差异对整体性能将产生巨大影响。
我们准备了一些测试来证明这一点。所有的代码都可以在 GitHub 上找到。你可以随意摆弄它。
我们期待您的反馈和贡献!
关于这个测试
测试应用程序使用 benchmark 库来获得具有统计意义的结果。测试的输入是一个包含一百万个对象的数组,其结构为 {a: number; b: number; r: number}。下面是生成这个数组的代码:
function generateTestArray() {
const result = [];
for (let i = 0; i < 1000000; ++i) {
result.push({
a: i,
b: i / 2,
r: 0,
});
}
return result;
}
我们使用了一台配有英特尔酷睿 i5-8250 的联想 T480s 笔记本电脑,16Gb RAM,运行 Ubuntu 20.04 LTS, Node v14.16.0 来获得结果。
Array.forEach 与 for / for..of 对比
每分钟操作数,数字越大越好
此测试计算每个数组元素的 a 和 b,并将结果存储到 r:
array.forEach((x) => {
x.r = x.a + x.b;
});
我们在数组生成期间特意 r 在对象中创建了一个字段,以避免更改对象结构,因为这会影响基准测试。
即使使用这些简单的测试,循环速度也几乎快了三倍。循环稍微领先于其余for..of部分,但差异并不显着。在某些其他语言中工作的循环的微优化for,例如缓存数组长度或将元素存储在临时变量中以供重复访问,对在 V8 上运行的 JavaScript 的影响为零。可能 V8 已经在幕后完成了它们。
由于.forEach与循环没有太大区别,for..of因此在大多数情况下,我们认为在传统循环上使用它没有多大意义。只有当您已经有一个函数可以在每个数组元素上调用时才值得使用。在这种情况下,它是单行代码,性能下降为零:
array.forEach(func);
Array.map 与 for / for..of 对比
每分钟操作数,数字越大越好
这些实验将一个数组映射到另一个数组,并在过程中执行 a + b:
return array.map((x) => x.a + x.b);
简单循环在这里的性能也快很多。for..of 创建一个空数组,并将每个新元素 push 进数组中
const result = [];
for (const { a, b } of array) {
result.push(a + b);
}
return result;
这不是最佳的方法,因为数组在每次循环中,都是动态地重新分配和移动的。而 for 循环版本预先为数组分配目标大小,并使用索引设置每个元素:
const result = new Array(array.length);
for (let i = 0; i < array.length; ++i) {
result[i] = array[i].a + array[i].b;
}
return result;
在这里,我们还测试了解构是否对性能有影响。当使用 .map 语句、相同的测试条件下,使用 for..of 的结果没有太大的区别,这可能只是一个偶然结果。
Array.reduce 和 for / for..of 对比
每分钟操作数,数字越大越好
这里我们只计算了数组中 a 和 b 的和:
return array.reduce((p, x) => p + x.a + x.b, 0);
for 和 for..of 都比 reduce 快 3.5 倍。但是,代码看起来要冗长得多:
let result = 0;
for (const { a, b } of array) {
result += a + b;
}
return result;
为了一个简单的和,写这么多代码行,一定需要很充分的理由。所以,除非性能非常关键,否则 .reduce 要好得多。测试再次显示两个循环之间没有差异。
结论
基准测试证明,使用循环的命令式编程比使用 Array 方法拥有更好的性能。使用回调函数的循环是值得考虑的,特别是对于大数据量的数组而言。然而,对于那些逻辑相比简单的求和来说更为复杂的代码,相对差异不会太大,因为计算的过程本身会花费更多时间。
命令式代码在大多数情况下要冗长得多。对于简单的求和,五行代码有点太多了,而 reduce 只有一行代码。另一方面,.forEach 的功能几乎等同于 for 或for..of,只是执行效率慢一点。两个循环之间没有太大的性能差异,你可以使用更加适合算法或逻辑的方法来使用。
与 AssemblyScript 不同,针对 for 循环的微小优化,对于 JavaScript 中的数组没有意义。V8 已经做得很好,甚至可能消除了越界访问的检查。
预先分配一个已知长度的数组比依赖于动态增长的 push 操作要快得多。我们还确认,解构的开销是很小的,只要方便就应该使用。
优秀的开发人员应该知道代码是如何工作的,并在每种情况下选择最佳解决方案。声明式编程以其简单性赢得了大多数时间。编写底层代码只在两种情况下有意义:
- 优化通过大量分析论证得出的瓶颈
- 明显存在性能瓶颈的代码
记住,过早的优化是万恶之源。
你可以随意克隆 GitHub 项目 来运行基准测试。