Skip to content

JavaScript 生成器函数:从执行模型到工程实践

核心结论

生成器函数(Generator Function)是 ES2015 引入的一种特殊函数,通过 function* 声明,在函数体内使用 yield 关键字实现"可暂停"的执行模型:每次执行到 yield 时函数暂停并向调用方返回一个值;调用方通过 next() 重新激活函数,并可同时向函数内部传入参数。这种"双向通信"机制把函数变成可被外部驱动的状态机,从而支撑惰性求值、无限序列、异步流程控制和显式状态机等场景。

本文从语法、协议、状态机、方法与工程应用五个维度展开讨论,覆盖 ES2015 同步生成器与 ES2018 异步生成器。

一、语法与基本形态

生成器函数使用 function* 声明:

javascript
function* gen() {
  yield 1;
  yield 2;
  yield 3;
}

const g = gen();
console.log(g);          // gen { <suspended> }
console.log(g.next());   // { value: 1, done: false }
console.log(g.next());   // { value: 2, done: false }
console.log(g.next());   // { value: 3, done: false }
console.log(g.next());   // { value: undefined, done: true }

要点:

  • 星号可以紧贴 functionfunction*)或写在中间(function *gen()),二者等价。
  • 调用生成器函数不会立即执行函数体,而是返回一个生成器对象。
  • 生成器对象实现了迭代器协议,可被 for...of... 展开、Array.from 等语法消费。
  • 函数体执行完毕或被 return() 终止后,后续 next() 始终返回 { value: undefined, done: true }

二、执行模型:状态机视角

生成器内部维护一个有限状态机,ECMAScript 规范定义了四个核心状态:

状态含义
suspended-start创建后尚未开始执行
executing当前正在执行
suspended-yieldyield 处暂停
completed函数体执行完毕或已终止

状态在外部驱动下的典型转换如下:

text
调用 gen()           → suspended-start
首次 next()          → executing → 命中 yield → suspended-yield
后续 next()          → executing → 命中下一个 yield → suspended-yield
函数 return / 抛错   → completed
return() / throw()   → completed

这意味着生成器对象在 suspended 状态下不会自动执行,必须由外部主动驱动一次 next() 才会推进一格。

三、next() 与双向通信

next() 是驱动生成器的核心方法,承担两种角色:

  1. 不传参数:恢复执行到下一个 yield
  2. 传入参数:恢复执行的同时,把该参数作为上一个 yield 表达式的返回值传入生成器。
javascript
function* gen() {
  const a = yield 'first';
  const b = yield 'second';
  return [a, b];
}

const g = gen();
console.log(g.next());    // { value: 'first', done: false }
console.log(g.next(10));  // { value: 'second', done: false }
console.log(g.next(20));  // { value: [10, 20], done: true }

注意:第一次 next() 不应传值。此时函数体还没开始执行,没有"上一个 yield"可以接收这个值,传入的值会被规范明确规定为忽略。若不通过 next 传值,yield 表达式的结果为 undefined

javascript
function* gen() {
  const x = yield 1;
  console.log(x);
}
const g = gen();
g.next();
g.next(); // 不会打印 10,打印 undefined

四、return()throw()

4.1 return(value):强制终止

调用 g.return(value) 会让生成器立即进入 completed 状态:

javascript
function* gen() {
  try {
    yield 1;
    yield 2;
  } finally {
    console.log('cleanup');
  }
}

const g = gen();
console.log(g.next());      // { value: 1, done: false }
console.log(g.return(99));  // { value: 99, done: true },finally 执行
console.log(g.next());      // { value: undefined, done: true }

try { ... } finally { ... } 是清理资源(关闭文件、取消订阅)的标准时机;只要函数还没退出 completedfinally 总会执行。

4.2 throw(error):在 yield 处抛出错误

g.throw(err) 会让生成器在下一次执行时(从暂停点开始)抛出错误:

javascript
function* gen() {
  try {
    yield 1;
    yield 2;
  } catch (e) {
    console.log('caught:', e.message);
  }
}

const g = gen();
g.next();                          // 走到 yield 1
g.throw(new Error('boom'));        // 在 yield 1 处抛错,被 catch 捕获
g.next();                          // { value: undefined, done: true }

throw()return() 让调用方可以主动控制生成器的执行,是双向通信的关键工具。

五、yield* 委托

yield* 把执行权委托给另一个生成器或任意可迭代对象:

javascript
function* inner() {
  yield 'a';
  yield 'b';
}

function* outer() {
  yield 1;
  yield* inner();
  yield 2;
}

console.log([...outer()]); // [1, 'a', 'b', 2]

语义:

  • 外层生成器把执行权交给内层可迭代对象。
  • 内层产生的每个值被透传给外层。
  • 内层结束时,外层从下一个 yield 继续。
  • 若外层通过 next(value)yield* 传值,该值会作为内层首个 next 调用的参数(前提是内层是生成器)。

六、协议层:Iterator 与 Iterable

生成器对象同时实现 ECMAScript 的两个协议:

协议关键角色
Iteratornext() 返回 { value, done }数据生产者
Iterable实现 [Symbol.iterator](),返回自身可被多种语法消费

这意味着生成器既能产生数据,又能被多种语法消费:

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

const arr = [...range(0, 5)]; // [0, 1, 2, 3, 4]
for (const n of range(0, 5)) {
  // ...
}

注意:生成器对象只能被消费一次。第二次迭代同一对象,会发现它已经处于 completed 状态。如果需要多次遍历,请重新调用生成器函数。

七、异步生成器

ES2018 引入了 async function*

javascript
async function* asyncGen() {
  yield await fetch('https://api.example.com/a');
  yield await fetch('https://api.example.com/b');
}

异步生成器每次 next() 返回 Promise<{ value, done }>,使用 for await...of 消费:

javascript
for await (const data of asyncGen()) {
  console.log(data);
}

异步生成器把同步迭代协议延伸到了异步数据源:分页 API、WebSocket 流、文件流都能以"同步写法"消费。

八、典型应用场景

8.1 无限序列

javascript
function* fibonacci() {
  let [a, b] = [0, 1];
  while (true) {
    yield a;
    [a, b] = [b, a + b];
  }
}

const fib = fibonacci();
fib.next().value; // 0
fib.next().value; // 1
fib.next().value; // 1
fib.next().value; // 2

生成器天然适合表示无限序列,因为只有外部 next() 时才计算下一个值,无需一次性展开内存。

8.2 惰性数据流

把"读取 → 处理"管线拆成多个生成器,按需驱动:

javascript
function* chunkify(iterable, size) {
  let chunk = [];
  for (const item of iterable) {
    chunk.push(item);
    if (chunk.length === size) {
      yield chunk;
      chunk = [];
    }
  }
  if (chunk.length) yield chunk;
}

8.3 状态机

每个 yield 即一个状态点:

javascript
function* trafficLight() {
  while (true) {
    yield 'red';
    yield 'green';
    yield 'yellow';
  }
}

8.4 异步流程控制

async/await 普及之前,生成器被广泛用于编写同步风格的异步代码(如 co 库)。async function 在内部使用 Promise 包装手动状态机,与生成器底层机制相似;async/await 是更专门的语法糖。如果想要多路并发结果同时收集,参考 从Promise到Generator和async&await 中的"等待赋值"写法。

九、与相邻方案的对比

9.1 与普通函数

维度普通函数生成器函数
执行模型一次性执行到底可暂停、可恢复
返回值单一返回值多次产出值
调用次数每次调用独立每个生成器对象独立
状态保存内部保存执行上下文

9.2 与 async/await

  • await 只能等待 Promise,yield 可产出任意值。
  • async/await 自动展开 try/catch;生成器需要手动 throw()
  • async/await 是同步代码的语法扩展;生成器是迭代协议的一部分。

ES2018 引入的 for await...of 弥补了差距,让生成器也能优雅处理异步数据流。

9.3 与 RxJS / Observable

Observable 是发布/订阅模式,适合多值异步事件流(事件、推送式数据)。生成器是拉模型,由外部主动拉取数据。两者适用场景不同:拉取式数据用生成器更直观,事件流用 Observable 更合适。

十、底层实现要点

不同 JavaScript 引擎实现细节不同,但基本思想一致:

  • 暂停时保存局部变量、参数、当前位置等执行上下文。
  • 再次 next() 时恢复这些状态继续执行。
  • V8 在内部为生成器生成了专门的字节码路径。

性能开销主要在暂停/恢复时的上下文保存,通常远小于一次完整函数调用,但仍不适用于极端高频的拉取场景。

十一、注意事项与边界

  1. 一次性消费:生成器对象只能消费一次。多次迭代需要重新调用生成器函数。
  2. 不能作为构造函数:new gen() 抛出 TypeError。
  3. 箭头函数不能是生成器:async () => yield 1 是非法的,必须使用 function*
  4. 不要 yield 副作用:例如 yield fs.write(...),因为无法保证消费者何时拉取。
  5. 错误处理:生成器内部的 try/catch 只能捕获同步错误;异步生成器中的 await 抛错需单独处理。
  6. 调试:调试器对生成器的支持因环境而异,必要时需在 yield 处设置断点。

十二、版本与兼容性

版本新增特性
ES2015 (ES6)生成器函数、yieldyield*
ES2018异步生成器 async function*for await...of

现代浏览器(Chrome 39+、Firefox 26+、Safari 10+)和 Node.js(4+)均已支持。

十三、总结

生成器函数提供的是"可暂停的同步执行模型 + 标准迭代器协议 + 双向通信通道"。判断何时使用:

  • 惰性求值或无限序列:生成器天然合适。
  • 表达状态机:每个 yield 即一个状态点。
  • 异步数据流:用异步生成器 + for await...of
  • 同步风格的异步:优先使用 async/await,除非需要兼容老环境。

理解生成器的关键不在于记住语法,而在于把握"函数执行从一次性变成可中断"这一根本变化:函数不再只是"调用 → 返回值",而是一段可由外部控制的、可持续的状态机。

参考资料

延伸阅读