Back

JavaScript 生成器的使用场景

JavaScript 生成器的使用场景

JavaScript 生成器(function*)自 ES2015 起就已成为语言的一部分,但许多前端开发者在生成器本应是更简洁选择的场景下,仍然习惯使用数组或 Promise 链。生成器的核心价值并非原始速度,而是惰性求值、可组合性以及对迭代的精确控制。本文将介绍生成器在现代前端代码中真正发挥作用的场景。

核心要点

  • 生成器通过惰性求值按需产生值,避免不必要的计算和中间数组
  • Iterator Helpers API 为生成器返回的迭代器提供了内置的 mapfiltertake 方法,无需自定义工具函数
  • yield* 使树和图的递归遍历既可读又具有惰性
  • 异步生成器(async function*)配合 for await...of 可以用最少的状态管理处理分页或批量数据获取

JavaScript 生成器的独特之处

生成器函数返回一个迭代器。调用该函数不会执行任何代码——它只是返回一个带有 .next() 方法的对象。每次调用 .next() 都会运行函数体直到下一个 yield,然后暂停,在多次调用之间保留局部状态。

function* range(start, end) {
  for (let i = start; i < end; i++) yield i
}

for (const n of range(0, 5)) {
  console.log(n) // 0, 1, 2, 3, 4
}

由于生成器实现了迭代器协议,它们可以直接与 for...of、展开语法和解构赋值配合使用——无需适配器。

JavaScript 中的惰性迭代:处理数据而不具体化它

使用生成器而非数组的主要原因是惰性迭代:值仅在被请求时才产生。这在以下情况下很重要:

  • 完整数据集很大但你只需要其中一部分
  • 计算每个值的成本很高
  • 序列在概念上是无限的
function* naturals() {
  let n = 0
  while (true) yield n++
}

// 只计算到断点之前的值
for (const n of naturals()) {
  if (n > 100) break
}

不会创建中间数组。不会计算断点之后的值。

Iterator Helpers API:内置的惰性管道

过去编写自定义的 mapfiltertake 工具函数是必要的样板代码。Iterator Helpers API——现已在所有现代浏览器中可用——将这些方法直接添加到同步迭代器上:

const result = naturals()
  .filter(n => n % 2 === 0)
  .map(n => n * n)
  .take(5)
  .toArray() // [0, 4, 16, 36, 64]

每个步骤都是惰性的。.toArray() 才会触发求值。这使得基于生成器的管道在不依赖第三方库的情况下显著更简洁。需要注意的是,这些辅助方法适用于同步迭代器——异步迭代器辅助方法尚未在各个环境中标准化。

树和图的遍历

生成器非常适合遍历递归结构。类 DOM 树的深度优先遍历变得简单明了:

function* walkTree(node) {
  yield node
  for (const child of node.children ?? []) {
    yield* walkTree(child)
  }
}

for (const node of walkTree(rootNode)) {
  if (node.type === 'input') processInput(node)
}

yield* 委托给嵌套的生成器,保持递归的可读性和遍历的惰性——一旦找到所需内容就可以立即停止。

JavaScript 中的异步生成器:分页和批量数据获取

async function* 将这一模式扩展到异步序列。结合 for await...of,它非常适合处理分页 API 响应:

async function* fetchPages(url) {
  let nextUrl = url
  while (nextUrl) {
    const res = await fetch(nextUrl)
    const data = await res.json()
    yield data.items
    nextUrl = data.nextPage ?? null
  }
}

for await (const batch of fetchPages('/api/records')) {
  processBatch(batch)
}

每一页只在循环推进时才获取。无需预先收集所有页面或在外部管理分页状态——生成器会保存这些状态。

何时不应使用生成器

生成器会增加间接性。对于需要完全消费的简单数组转换,链式数组方法更清晰。当序列很大、可能无限,或者需要提前停止而不浪费计算时,才使用生成器。

总结

JavaScript 生成器在三个领域表现出色:对大型或无限序列的惰性迭代、可组合的数据管道(特别是配合 Iterator Helpers API),以及需要顺序、有状态控制的异步数据获取。它们不是数组或 async/await 的替代品——它们是在需要按需产生值而非一次性产生所有值时的正确工具。

常见问题

可以。生成器非常适合产生供 React 组件消费的数据序列。你可以在 useEffect 或 useMemo hook 中调用生成器来惰性计算值。但是,不要将生成器用作组件函数本身——React 期望组件返回 JSX,而不是迭代器。

生成器会在其最后一个 yield 点保持暂停状态。一旦对其迭代器对象的所有引用都消失,它就会被垃圾回收。如果需要在迭代提前停止时运行清理逻辑,可以将 yield 包装在 try-finally 块中。当迭代器的 return 方法被调用或生成器被垃圾回收时,finally 块会执行。

对于完全消费的小型集合,生成器由于暂停-恢复机制会有轻微的开销。在大多数应用中,性能差异可以忽略不计。当你部分处理大型数据集时,生成器在实践中会更快,因为它们避免了分配中间数组,并跳过了你从未请求的值的计算。

异步生成器在值可用时逐步产生它们,而基于 Promise 的方法会等到收集完所有数据后才返回。这意味着异步生成器让你可以立即开始处理第一批结果,减少峰值内存使用,并让你更精细地控制每次后续获取的时机。

Complete picture for complete understanding

Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.

Check our GitHub repo and join the thousands of developers in our community.

OpenReplay