for...of 和 for await...of 都是 ES6 之后引入的迭代语句,用于遍历可迭代对象 (Iterable)中的值。它们的核心区别在于处理的迭代协议不同:一个是同步迭代 ,另一个是异步迭代。下面从协议基础、行为差异、适用场景和底层细节展开对比。
1. 迭代协议基础
| 特性 | for...of |
for await...of |
|---|---|---|
| 依赖的迭代协议 | Symbol.iterator |
Symbol.asyncIterator |
| 迭代器返回的方法 | next() 直接返回 { value, done } |
next() 返回一个 Promise<{ value, done }> |
| 适用的可迭代对象 | 数组、字符串、Map、Set、arguments、NodeList、生成器对象(同步)等 | 异步生成器、ReadableStream、实现了 Symbol.asyncIterator 的对象、同步可迭代对象(会被自动适配) |
| 是否可在非 async 函数内使用 | ✅ 任何地方 | ✅ 任何地方(但循环体内若使用 await 则必须包裹在 async 函数中) |
2. 核心区别详解
2.1 迭代值的获取方式
for...of:同步获取下一个值,若迭代器返回的是 Promise 则不会被等待,直接将 Promise 对象作为值赋给循环变量。
javascript
const iterable = {
[Symbol.iterator]() {
let i = 0;
return {
next() {
if (i++ < 2) {
return { value: Promise.resolve(i), done: false };
}
return { done: true };
}
};
}
};
for (const x of iterable) {
console.log(x); // Promise { <resolved>: 1 } 两次
}
for await...of:会等待 迭代器的next()返回的 Promise 完成,并将 fulfilled 的值赋给循环变量。
javascript
const asyncIterable = {
[Symbol.asyncIterator]() {
let i = 0;
return {
async next() {
if (i++ < 2) {
return { value: i, done: false };
}
return { done: true };
}
};
}
};
for await (const x of asyncIterable) {
console.log(x); // 1, 2(数字,非 Promise)
}
2.2 对同步可迭代对象的兼容性
for...of只能直接遍历同步可迭代对象。for await...of也能遍历同步可迭代对象(如数组、Set),此时它会将同步返回的{ value, done }中的value自动包装成已解决的 Promise,然后等待其结果(其实瞬间完成)。
javascript
const arr = [1, 2, 3];
// 正常打印 1, 2, 3
for (const v of arr) console.log(v);
// 同样正常打印 1, 2, 3,但有细微的异步开销
for await (const v of arr) console.log(v);
因此 for await...of 是一个更宽松的循环,它既能处理异步迭代器,也能处理同步迭代器(只不过在同步迭代器上会多一次 Promise 转换和 microtask 调度)。
2.3 循环体内的异步操作支持
for...of循环体内部可以使用await,但前提是整个循环必须位于async函数内 ,且await并不会影响迭代器获取下一个值------循环仍然同步推进,只是循环体内的异步操作被挂起。
javascript
async function demo() {
for (const url of urls) {
const data = await fetch(url); // 每次迭代等待 fetch 完成,但迭代本身是同步推进的
}
}
for await...of会在每次迭代获取下一个值时自动等待 (即等待next()的 Promise),循环体内部可以继续使用await做其他异步操作。
2.4 错误处理
- 若
for...of的迭代器抛出同步异常,会被try...catch捕获。 - 若
for await...of的迭代器next()返回的 Promise 被拒绝,或异步生成器内部抛出异常,该异常也会在循环内被捕获(因为循环本身会 await 该 Promise)。
3. 适用场景对比
✅ 使用 for...of 的场景
- 遍历同步数据结构:数组、字符串、Map、Set、TypedArray、arguments、DOM 集合等。
- 处理同步生成器函数返回的生成器对象。
- 循环体需要执行异步操作,但获取下一个迭代值的时机不依赖异步结果。
✅ 使用 for await...of 的场景
-
遍历异步数据流:
ReadableStream(浏览器流 API、Node.js 流通过stream[Symbol.asyncIterator])- 异步生成器函数(
async function*)返回的对象 - 分页 API 数据,需要逐页等待获取下一页
-
处理以 Promise 形式逐个提供值的数据源。
-
需要顺序处理异步任务,且每个任务的触发依赖于上一个任务完成后的状态(例如数据库游标逐条读取)。
4. 底层协议深入
Symbol.iterator vs Symbol.asyncIterator
| 协议 | 方法名 | next() 返回值 |
return() / throw() 返回值 |
|---|---|---|---|
| 同步迭代器 | [Symbol.iterator] |
{ value, done } |
{ value, done } 或抛出同步异常 |
| 异步迭代器 | [Symbol.asyncIterator] |
Promise<{ value, done }> |
Promise<{ value, done }> |
for await...of 实际上在引擎内部会调用对象的 [Symbol.asyncIterator] 方法,若该方法不存在,则会回退 到 [Symbol.iterator],并将同步返回的值包装成 Promise.resolve(value)。这也是为什么 for await...of 能遍历同步可迭代对象。
5. 示例对比
示例 1:遍历异步生成器
javascript
async function* generateNumbers() {
yield 1;
yield 2;
yield 3;
}
// ❌ 错误:TypeError: generateNumbers() is not iterable
for (const num of generateNumbers()) { }
// ✅ 正确
for await (const num of generateNumbers()) {
console.log(num); // 1, 2, 3
}
示例 2:遍历数组(对比异步等待行为)
javascript
const urls = ['/api/1', '/api/2', '/api/3'];
// 在 async 函数中用 for...of
async function fetchAll() {
for (const url of urls) {
const res = await fetch(url); // 循环等待每次 fetch 完成才进入下一次迭代
console.log(await res.json());
}
}
// 用 for await...of 遍历同一个数组(没有必要但合法)
async function fetchAll2() {
for await (const url of urls) {
const res = await fetch(url);
console.log(await res.json());
}
}
// 两者行为相同,但 fetchAll2 每次迭代会额外创建一个 Promise 包装 url 字符串
示例 3:读取流数据(Node.js 可读流)
javascript
import { createReadStream } from 'fs';
const stream = createReadStream('./file.txt', { encoding: 'utf8' });
for await (const chunk of stream) {
console.log(chunk);
}
6. 性能与注意事项
- 性能差异 :
for await...of遍历同步可迭代对象时,由于需要将每个值包装成 Promise 并在 microtask 中展开,会带来额外开销。不建议 将for await...of用于纯同步数据的遍历。 - 不能在普通对象上使用 :两者都要求对象实现对应的迭代器协议,普通对象
{}既没有[Symbol.iterator]也没有[Symbol.asyncIterator],因此不能直接使用for...of或for await...of。 for await...of与循环内await的关系 :即使没有在循环体内显式写await,for await...of本身也会在每次迭代时对next()的结果进行await,因此整体执行是异步的。这意味着循环后的代码会在循环完全结束后才执行。
7. 总结对照表
| 维度 | for...of |
for await...of |
|---|---|---|
| 迭代协议 | Symbol.iterator |
Symbol.asyncIterator(优先) |
| 处理异步值 | 不等待,直接得到 Promise 对象 | 等待 Promise 解析后得到值 |
| 遍历同步可迭代对象 | ✅ 原生支持 | ✅ 支持(有额外 Promise 包装) |
| 遍历异步可迭代对象 | ❌ 报错或得到 Promise 对象 | ✅ 原生支持 |
| 主要用途 | 同步数据遍历 | 异步数据流、异步生成器遍历 |
循环体内 await 的影响 |
仅暂停循环体,不改变迭代推进时机 | 既等待迭代器 Promise,也可在循环体内 await |
| 错误传播 | 同步异常直接抛出 | 异步拒绝被捕获为异常 |
一句话总结:
for...of 用于同步迭代值;for await...of 用于异步迭代值,它会在每次迭代时自动等待 Promise 解析,适合处理流式数据、异步生成器以及需要按顺序等待异步结果的场景。