**译者按:** JS 骚操作。
- 原文:[For vs forEach() vs for/in vs for/of in JavaScript](http://thecodebarbarian.com/for-vs-for-each-vs-for-in-vs-for-of-in-javascript.html)
- 译者: [Fundebug](https://www.fundebug.com/)
**本文采用意译,版权归原作者所有**
我们有多种方法来遍历 JavaScript 的数组或者对象,而它们之间的区别非常让人[疑惑](https://stackoverflow.com/questions/9329446/for-each-over-an-array-in-javascript)。[Airbnb 编码风格](http://airbnb.io/javascript/#iterators--nope)禁止使用 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`,我们可以访问数组的下标,而不是实际的数组元素值:
```javascript
for (let i = 0; i < arr.length; ++i) {
console.log(arr[i]);
}
for (let i in arr) {
console.log(arr[i]);
}
```
使用`for/of`,则可以直接访问数组的元素值:
```javascript
for (const v of arr) {
console.log(v);
}
```
使用`forEach()`,则可以同时访问数组的下标与元素值:
```javascript
arr.forEach((v, i) => console.log(v));
```
### 非数字属性
JavaScript 的数组就是 Object,这就意味着我们可以给数组添加字符串属性:
```javascript
const arr = ["a", "b", "c"];
typeof arr; // 'object'
arr.test = "bad"; // 添加非数字属性
arr.test; // 'abc'
arr[1] === arr["1"]; // true, JavaScript数组只是特殊的Object
```
4 种循环语法,只有`for/in`不会忽略非数字属性:
```javascript
const arr = ["a", "b", "c"];
arr.test = "bad";
for (let i in arr) {
console.log(arr[i]); // 打印"a, b, c, bad"
}
```
正因为如此,[使用`for/in`遍历数组并不好](https://stackoverflow.com/questions/500504/why-is-using-for-in-with-array-iteration-a-bad-idea)。
其他 3 种循环语法,都会忽略非数字属性:
```javascript
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](https://eslint.org/docs/rules/guard-for-in)规则来禁止使用`for/in`。
### 数组的空元素
JavaScript 数组可以有[空元素](https://stackoverflow.com/questions/281264/remove-empty-elements-from-an-array-in-javascript)。以下代码语法是正确的,且数组长度为 3:
```javascript
const arr = ["a", , "c"];
arr.length; // 3
```
让人更加不解的一点是,循环语句处理`['a',, 'c']`与`['a', undefined, 'c']`的方式并不相同。
对于`['a',, 'c']`,`for/in`与`forEach`会跳过空元素,而`for`与`for/of`则不会跳过。
```javascript
// 打印"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"。
还有一种添加空元素的方式:
```javascript
// 等价于`['a', 'b', 'c',, 'e']`
const arr = ["a", "b", "c"];
arr[5] = "e";
```
还有一点,JSON 也不支持空元素:
```javascript
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"](http://2ality.com/2013/07/array-iteration-holes.html)。如果你想避免这个问题,可以考虑禁用`forEach`:
```yml
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 测试下面的代码,结果如下:
```javascript
"use strict";
const arr = ["a"];
arr.forEach(function() {
console.log(this); // 打印undefined
});
arr.forEach(() => {
console.log(this); // 打印{}
});
```
**要点:** 使用 ESLint 的[`no-arrow-callback`](https://eslint.org/docs/rules/prefer-arrow-callback)规则要求所有回调函数必须使用箭头函数。
### Async/Await 与 Generators
还有一点,`forEach()`不能与 Async/Await 及 Generators 很好的"合作"。
不能在`forEach`回调函数中使用 await:
```javascript
async function run() {
const arr = ['a', 'b', 'c'];
arr.forEach(el => {
// SyntaxError
await new Promise(resolve => setTimeout(resolve, 1000));
console.log(el);
});
}
```
不能在`forEach`回调函数中使用 yield:
```javascript
function* run() {
const arr = ['a', 'b', 'c'];
arr.forEach(el => {
// SyntaxError
yield new Promise(resolve => setTimeout(resolve, 1000));
console.log(el);
});
}
```
对于`for/of`来说,则没有这个问题:
```javascript
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`[按照顺序执行](https://thecodebarbarian.com/basic-functional-programming-with-async-await.html),则会比较头疼。
下面的代码会按照从大到小打印 0-9:
```javascript
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`获取数组索引,可以这样写:
```javascript
for (const [i, v] of arr.entries()) {
console.log(i, v);
}
```
### 参考
- [For-each over an array in JavaScript?](https://stackoverflow.com/questions/9329446/for-each-over-an-array-in-javascript)
- [Why is using “for…in” with array iteration a bad idea?](https://stackoverflow.com/questions/500504/why-is-using-for-in-with-array-iteration-a-bad-idea)
- [Array iteration and holes in JavaScript](http://2ality.com/2013/07/array-iteration-holes.html)
### 关于Fundebug
[Fundebug](https://www.fundebug.com/)专注于JavaScript、微信小程序、微信小游戏、支付宝小程序、React Native、Node.js和Java线上应用实时BUG监控。 自从2016年双十一正式上线,Fundebug累计处理了10亿+错误事件,付费客户有Google、360、金山软件、百姓网等众多品牌企业。欢迎大家[免费试用](https://www.fundebug.com/team/create)!

- 如何高效地遍历超大MongoDB集合
- 深入理解 JavaScript 执行上下文和执行栈
- JavaScript 为什么要有 Symbol 类型?
- Web 性能优化:21 种优化 CSS 和加快网站速度的方法
- 深入理解 JavaScript 作用域和作用域链
- WEB 实时推送技术的总结
- 前端面试:谈谈 JS 垃圾回收机制
- TypeScript,初次见面,请多指教 ?
- 使用这些 HTTP 头保护 Web 应用
- JavaScript 的 4 种数组遍历方法: for VS forEach() VS for/in VS for/of
- 装上这几个 VSCode 插件后,上班划水摸鱼不是梦
- 一文搞懂TCP与UDP的区别
- 如何优雅地查看 JS 错误堆栈?
- 一文读懂 HTTP/2 及 HTTP/3 特性
- Web 性能优化: 图片优化让网站大小减少 62%
- 我们应该如何给需求排序?
- Web 性能优化: 使用 Webpack 分离数据的正确方法
- 2019 前端面试题汇总(主要为 Vue)
- 高效使用VSCode的9点建议
- 小程序多端框架全面测评:chameleon、Taro、uni-app、mpvue、WePY
- 灵活使用 console 让 js 调试更简单
- 深入了解浏览器存储:对比Cookie、LocalStorage、sessionStorage与IndexedDB
- JavaScript是如何工作的:引擎,运行时和调用堆栈的概述!
- Web 性能优化:Preload与Prefetch的使用及在 Chrome 中的优先级
- 如何来一次说干就干的重构 (流程篇)
- JavaScript 新语法详解:Class 的私有属性与私有方法
- JavaScript 原型的深入指南
- 一个程序员的自我修养
- 为什么HTTPS比HTTP更安全?