乐趣区

JavaScript 的 4 种数组遍历方法: for VS forEach() VS for/in VS for/of

译者按:JS 骚操作。

原文:For vs forEach() vs for/in vs for/of in JavaScript

译者: Fundebug

本文采用意译,版权归原作者所有
我们有多种方法来遍历 JavaScript 的数组或者对象,而它们之间的区别非常让人疑惑。Airbnb 编码风格禁止使用 for/in 与 for/of,你知道为什么吗?
这篇文章将详细介绍以下 4 种循环语法的区别:

for (let i = 0; i < arr.length; ++i)
arr.forEach((v, i) => {/* … */})
for (let i in arr)
for (const v of arr)

语法
使用 for 和 for/in,我们可以访问数组的下标,而不是实际的数组元素值:
for (let i = 0; i < arr.length; ++i) {
console.log(arr[i]);
}

for (let i in arr) {
console.log(arr[i]);
}
使用 for/of,则可以直接访问数组的元素值:
for (const v of arr) {
console.log(v);
}
使用 forEach(),则可以同时访问数组的下标与元素值:
arr.forEach((v, i) => console.log(v));
非数字属性
JavaScript 的数组就是 Object,这就意味着我们可以给数组添加字符串属性:
const arr = [“a”, “b”, “c”];

typeof arr; // ‘object’

arr.test = “bad”; // 添加非数字属性

arr.test; // ‘abc’
arr[1] === arr[“1”]; // true, JavaScript 数组只是特殊的 Object
4 种循环语法,只有 for/in 不会忽略非数字属性:
const arr = [“a”, “b”, “c”];
arr.test = “bad”;

for (let i in arr) {
console.log(arr[i]); // 打印 ”a, b, c, bad”
}
正因为如此,使用 for/in 遍历数组并不好。
其他 3 种循环语法,都会忽略非数字属性:
const arr = [“a”, “b”, “c”];
arr.test = “abc”;

// 打印 “a, b, c”
for (let i = 0; i < arr.length; ++i) {
console.log(arr[i]);
}

// 打印 “a, b, c”
arr.forEach((el, i) => console.log(i, el));

// 打印 “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 与 forEach 会跳过空元素,而 for 与 for/of 则不会跳过。
// 打印 ”a, undefined, c”
for (let i = 0; i < arr.length; ++i) {
console.log(arr[i]);
}

// 打印 ”a, c”
arr.forEach(v => console.log(v));

// 打印 ”a, c”
for (let i in arr) {
console.log(arr[i]);
}

// 打印 ”a, undefined, c”
for (const v of arr) {
console.log(v);
}
对于[‘a’, undefined, ‘c’],4 种循环语法一致,打印的都是 ”a, undefined, c”。
还有一种添加空元素的方式:
// 等价于 `[‘a’, ‘b’, ‘c’,, ‘e’]`
const arr = [“a”, “b”, “c”];
arr[5] = “e”;
还有一点,JSON 也不支持空元素:
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
要点:for/in 与 forEach 会跳过空元素,数组中的空元素被称为 ”holes”。如果你想避免这个问题,可以考虑禁用 forEach:
parserOptions:
ecmaVersion: 2018
rules:
no-restricted-syntax:
– error
– selector: CallExpression[callee.property.name=”forEach”]
message: Do not use `forEach()`, use `for/of` instead
函数的 this
for,for/in 与 for/of 会保留外部作用域的 this。
对于 forEach,除非使用箭头函数,它的回调函数的 this 将会变化。
使用 Node v11.8.0 测试下面的代码,结果如下:
“use strict”;

const arr = [“a”];

arr.forEach(function() {
console.log(this); // 打印 undefined
});

arr.forEach(() => {
console.log(this); // 打印{}
});
要点:使用 ESLint 的 no-arrow-callback 规则要求所有回调函数必须使用箭头函数。
Async/Await 与 Generators
还有一点,forEach()不能与 Async/Await 及 Generators 很好的 ” 合作 ”。
不能在 forEach 回调函数中使用 await:
async function run() {
const arr = [‘a’, ‘b’, ‘c’];
arr.forEach(el => {
// SyntaxError
await new Promise(resolve => setTimeout(resolve, 1000));
console.log(el);
});
}
不能在 forEach 回调函数中使用 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) {
// 打印 0 之前等待 1 秒,打印 1 之前等待 0.9 秒
await new Promise(resolve => setTimeout(() => resolve(), 1000 – n * 100));
console.log(n);
}

async function test() {
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9].forEach(print);
}

test();
要点:尽量不要在 forEach 中使用 aysnc/await 以及 generators。
结论
简单地说,for/of 是遍历数组最可靠的方式,它比 for 循环简洁,并且没有 for/in 和 forEach()那么多奇怪的特例。for/of 的缺点是我们取索引值不方便,而且不能这样链式调用 forEach(). forEach()。
使用 for/of 获取数组索引,可以这样写:
for (const [i, v] of arr.entries()) {
console.log(i, v);
}
参考

For-each over an array in JavaScript?
Why is using“for…in”with array iteration a bad idea?
Array iteration and holes in JavaScript

关于 Fundebug
Fundebug 专注于 JavaScript、微信小程序、微信小游戏、支付宝小程序、React Native、Node.js 和 Java 线上应用实时 BUG 监控。自从 2016 年双十一正式上线,Fundebug 累计处理了 10 亿 + 错误事件,付费客户有 Google、360、金山软件、百姓网等众多品牌企业。欢迎大家免费试用!

版权声明
转载时请注明作者 Fundebug 以及本文地址:https://blog.fundebug.com/2019/03/11/4-ways-to-loop-array-inj-javascript/

退出移动版