原文:For vs forEach() vs for/in vs for/of in JavaScript
在 JavaScript 中有许多方法来遍历一个对象或者数组,他们之间的差别是一个令许多人人困惑的 问题。
一些编码 规范 甚至禁止使用某些方法遍历。
在这篇文章中,我将解释以下 4 种方法遍历一个数组的不同:
for (let i = 0; i < arr.length; ++i)
arr.forEach((v, i) => { /* ... */ })
for (let i in arr)
for (const v of arr)
我将使用几种不同的边界情况概述这些循环结构之间的区别。我还将提供相关的 ESLint 规则链接,以便在你的项目中使用循环结构的最佳实践。
语法概述
for
和 for/in
结构让你可以得到数组索引,但不是真正的元素。例如,假设你想打印出下面这个数组中存储的内容:
const arr = ['a', 'b', 'c'];
使用 for
或者 for/in
,你需要打印 arr[i]
:
for (let i = 0; i < arr.length; ++i) {
console.log(arr[i]);
}
for (let i in arr) {
console.log(arr[i]);
}
使用另外两种方法, forEach()
和 for/of
,你可以得到元素本身。 forEach()
也可以得到元素索引,但 for/of
不可以。
arr.forEach((v, i) => console.log(v));
for (const v of arr) {
console.log(v);
}
非数值属性
JavaScript 中的数组也是对象。这意味着你不仅可以添加数字,也可以添加字符串属性到数组内。
const arr = ['a', 'b', 'c'];
typeof arr; // 'object'
// Assign to a non-numeric property
arr.test = 'bad';
arr.test; // 'bad'
arr[1] === arr['1']; // true, JavaScript arrays are just special objects
四种方法中的三种都忽视了非数值属性。只有 for/in
可以打印出’bad’:
const arr = ['a', 'b', 'c'];
arr.test = 'bad';
// Prints "a, b, c, bad"
for (let i in arr) {
console.log(arr[i]);
}
这就是为什么 使用 for/in
来遍历一个数组一般不是一个好主意的原因。其他的遍历方法都正确的忽视了非数值属性:
const arr = ['a', 'b', 'c'];
arr.test = 'abc';
// Prints "a, b, c"
for (let i = 0; i < arr.length; ++i) {
console.log(arr[i]);
}
// Prints "a, b, c"
arr.forEach((el, i) => console.log(i, el));
// Prints "a, b, c"
for (const el of arr) {
console.log(el);
}
重点: 避免在数组中使用 for/in
循环,除非你清楚你想要遍历非数值键和继承的键。使用 ESLint 规则 guard-for-in
来禁止使用 for/in
。
空元素
JavaScript 数组允许 空元素。下面这个数组是语法正确的,而且长度为 3:
const arr = ['a', , 'c'];
arr.length; // 3
让人更困惑的是不同循环结构对待 ['a',, 'c']
和 ['a', undefined, 'c']
不同。下面是四种循环结构如何对待像 ['a',, 'c']
这样有一个空元素的方式。 for/in
和 for/each
跳过了空元素, for
和 for/of
并没有。
// Prints "a, undefined, c"
for (let i = 0; i < arr.length; ++i) {
console.log(arr[i]);
}
// Prints "a, c"
arr.forEach(v => console.log(v));
// Prints "a, c"
for (let i in arr) {
console.log(arr[i]);
}
// Prints "a, undefined, c"
for (const v of arr) {
console.log(v);
}
如果你想知道的话,对于 ['a', undefined, 'c']
四种方法都会打印出 “a, undefined, c”。
这里有另外一种向数组中添加空元素的方法:
// Equivalent to `['a', 'b', 'c',, 'e']`
const arr = ['a', 'b', 'c'];
arr[5] = 'e';
forEach()
和 for/in
跳过了数组中的空元素,但 for
和 for/of
没有。 forEach()
的行为可能会导致一些问题,然而,JavaScript 数组中的空元素一般是很罕见的,因为它们不被 JSON 支持:
$ node
> JSON.parse('{"arr":["a","b","c"]}')
{ arr: [ 'a', 'b', 'c' ] }
> JSON.parse('{"arr":["a",null,"c"]}')
{ arr: [ 'a', null, 'c' ] }
> JSON.parse('{"arr":["a",,"c"]}')
SyntaxError: Unexpected token , in JSON at position 12
所以你不必担心用户数据中的空元素,除非你给了用户完全访问 JavaScript 运行时的权限。
重点: for/in
和 forEach()
跳过了数组中的空元素,也被称作 “holes”。很少有情况将空元素视为特殊情况而不是 undefined
。如果你关心空元素的特殊情况,下面是一个 .eslintrc.yml
的例子来禁止调用 forEach()
。
parserOptions:
ecmaVersion: 2018
rules:
no-restricted-syntax:
- error
- selector: CallExpression[callee.property.name="forEach"]
message: Do not use `forEach()` , use `for/of` instead
函数上下文 (Function Context)
Function context 是一个很好的用来表示 this 所指对象的方法。 for
、 for/in
和 for/of
中的 this 保持了和外面一样的指向,但 forEach()
回调将会有一个不同的 this,除非你使用 箭头函数。
'use strict';
const arr = ['a'];
// Prints "undefined"
arr.forEach(function() {
console.log(this);
});
重点: 使用 forEach()
时使用箭头函数。使用 no-arrow-callback
ESLint rule 来对不使用 this 的回调函数要求使用箭头函数。
Async/Await 和 生成器 (Generators)
另一种使用 forEach()
的边界情况是它 不能与 async/await 或者 generators 使用。如果你的 forEach()
回调是同步的,那没问题,但你不能在 forEach()
回调中使用 await
:
async function run() {
const arr = ['a', 'b', 'c'];
arr.forEach(el => {
// SyntaxError
await new Promise(resolve => setTimeout(resolve, 1000));
console.log(el);
});
}
你也不能使用 yield
:
function* run() {
const arr = ['a', 'b', 'c'];
arr.forEach(el => {
// SyntaxError
yield new Promise(resolve => setTimeout(resolve, 1000));
console.log(el);
});
}
上面的例子在 for/of
下是正常的:
async function asyncFn() {
const arr = ['a', 'b', 'c'];
for (const el of arr) {
await new Promise(resolve => setTimeout(resolve, 1000));
console.log(el);
}
}
function* generatorFn() {
const arr = ['a', 'b', 'c'];
for (const el of arr) {
yield new Promise(resolve => setTimeout(resolve, 1000));
console.log(el);
}
}
即使你在 forEach()
回调上标记了 async
,在尝试让异步 forEach()
顺序执行时,会感到非常头疼。例如,下面的程序将会将 0-9 用相反的顺序打印出来。
async function print(n) {
// Wait 1 second before printing 0, 0.9 seconds before printing 1, etc.
await new Promise(resolve => setTimeout(() => resolve(), 1000 - n * 100));
// Will usually print 9, 8, 7, 6, 5, 4, 3, 2, 1, 0 but order is not strictly
// guaranteed.
console.log(n);
}
async function test() {
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9].forEach(print);
}
test();
重点: 如果你使用 async/await 或者 generators,记住 forEach()
是语法糖。它应该小心的使用并且不要将它用于所有情况。
总结
一般来说, for/of
是用来遍历数组的方法中最健壮的。它比传统的 for
循环更加简洁,也没有像 for/in
和 forEach()
那样多的边界情况。 for/of
的主要缺点是需要使用额外的手段来得到索引[1] ,也不能像 forEach()
一样链式使用。使用 forEach()
有许多需要注意的地方,它应该被谨慎的使用,但在很多情况下它能让代码变得更简洁。
[1] 要在 for/of
循环中得到当前数组的索引,可以使用 Array.entries()
函数.
for (const [i, v] of arr.entries()) {
console.log(i, v); // Prints "0 a", "1 b", "2 c"
}