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

for...offor 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...offor await...of
  • for await...of 与循环内 await 的关系 :即使没有在循环体内显式写 awaitfor 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 解析,适合处理流式数据、异步生成器以及需要按顺序等待异步结果的场景。

相关推荐
REDcker2 小时前
Safari 26.4 新增 WebTransport:对 iOS WebView 的影响与落地建议
前端·ios·safari
练习前端两年半2 小时前
Vue3 KeepAlive 深度揭秘:组件缓存的魔法是如何实现的?
前端·vue.js·面试
吃西瓜的年年2 小时前
react(四)
前端·javascript·react.js
阿凤212 小时前
后端返回数据流的格式
开发语言·前端·javascript·uniapp
懂懂tty2 小时前
React Hooks原理
前端·react.js
00后程序员张2 小时前
前端可视化大屏制作全指南:需求分析、技术选型与性能优化
前端·ios·性能优化·小程序·uni-app·iphone·需求分析
kyriewen2 小时前
屎山代码拆不动?微前端来救场:一个应用变“乐高城堡”
前端·javascript·前端框架
@大迁世界2 小时前
3月 React 圈又变天了
前端·javascript·react.js·前端框架·ecmascript
忆江南3 小时前
# iOS 稳定性方向常见面试题与详解
前端