详细讲解下 for...of vs for await...of 区别

35 阅读6分钟

for...of 和 for await...of 都是 ES6 之后引入的迭代语句,用于遍历可迭代对象(Iterable)中的值。它们的核心区别在于处理的迭代协议不同:一个是同步迭代,另一个是异步迭代。下面从协议基础、行为差异、适用场景和底层细节展开对比。


1. 迭代协议基础

特性for...offor await...of
依赖的迭代协议Symbol.iteratorSymbol.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 对象作为值赋给循环变量。
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 的值赋给循环变量。
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,然后等待其结果(其实瞬间完成)。
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 并不会影响迭代器获取下一个值——循环仍然同步推进,只是循环体内的异步操作被挂起。
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:遍历异步生成器

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:遍历数组(对比异步等待行为)

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 可读流)

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 的关系:即使没有在循环体内显式写 awaitfor await...of 本身也会在每次迭代时对 next() 的结果进行 await,因此整体执行是异步的。这意味着循环后的代码会在循环完全结束后才执行。

7. 总结对照表

维度for...offor await...of
迭代协议Symbol.iteratorSymbol.asyncIterator(优先)
处理异步值不等待,直接得到 Promise 对象等待 Promise 解析后得到值
遍历同步可迭代对象✅ 原生支持✅ 支持(有额外 Promise 包装)
遍历异步可迭代对象❌ 报错或得到 Promise 对象✅ 原生支持
主要用途同步数据遍历异步数据流、异步生成器遍历
循环体内 await 的影响仅暂停循环体,不改变迭代推进时机既等待迭代器 Promise,也可在循环体内 await
错误传播同步异常直接抛出异步拒绝被捕获为异常

一句话总结:
for...of 用于同步迭代值;for await...of 用于异步迭代值,它会在每次迭代时自动等待 Promise 解析,适合处理流式数据、异步生成器以及需要按顺序等待异步结果的场景。