JavaScript 生成器函数:从执行模型到工程实践
核心结论
生成器函数(Generator Function)是 ES2015 引入的一种特殊函数,通过 function* 声明,在函数体内使用 yield 关键字实现"可暂停"的执行模型:每次执行到 yield 时函数暂停并向调用方返回一个值;调用方通过 next() 重新激活函数,并可同时向函数内部传入参数。这种"双向通信"机制把函数变成可被外部驱动的状态机,从而支撑惰性求值、无限序列、异步流程控制和显式状态机等场景。
本文从语法、协议、状态机、方法与工程应用五个维度展开讨论,覆盖 ES2015 同步生成器与 ES2018 异步生成器。
一、语法与基本形态
生成器函数使用 function* 声明:
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 }要点:
- 星号可以紧贴
function(function*)或写在中间(function *gen()),二者等价。 - 调用生成器函数不会立即执行函数体,而是返回一个生成器对象。
- 生成器对象实现了迭代器协议,可被
for...of、...展开、Array.from等语法消费。 - 函数体执行完毕或被
return()终止后,后续next()始终返回{ value: undefined, done: true }。
二、执行模型:状态机视角
生成器内部维护一个有限状态机,ECMAScript 规范定义了四个核心状态:
| 状态 | 含义 |
|---|---|
suspended-start | 创建后尚未开始执行 |
executing | 当前正在执行 |
suspended-yield | 在 yield 处暂停 |
completed | 函数体执行完毕或已终止 |
状态在外部驱动下的典型转换如下:
调用 gen() → suspended-start
首次 next() → executing → 命中 yield → suspended-yield
后续 next() → executing → 命中下一个 yield → suspended-yield
函数 return / 抛错 → completed
return() / throw() → completed这意味着生成器对象在 suspended 状态下不会自动执行,必须由外部主动驱动一次 next() 才会推进一格。
三、next() 与双向通信
next() 是驱动生成器的核心方法,承担两种角色:
- 不传参数:恢复执行到下一个
yield。 - 传入参数:恢复执行的同时,把该参数作为上一个
yield表达式的返回值传入生成器。
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:
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 状态:
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 { ... } 是清理资源(关闭文件、取消订阅)的标准时机;只要函数还没退出 completed,finally 总会执行。
4.2 throw(error):在 yield 处抛出错误
g.throw(err) 会让生成器在下一次执行时(从暂停点开始)抛出错误:
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* 把执行权委托给另一个生成器或任意可迭代对象:
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 的两个协议:
| 协议 | 关键 | 角色 |
|---|---|---|
| Iterator | next() 返回 { value, done } | 数据生产者 |
| Iterable | 实现 [Symbol.iterator](),返回自身 | 可被多种语法消费 |
这意味着生成器既能产生数据,又能被多种语法消费:
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*:
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 消费:
for await (const data of asyncGen()) {
console.log(data);
}异步生成器把同步迭代协议延伸到了异步数据源:分页 API、WebSocket 流、文件流都能以"同步写法"消费。
八、典型应用场景
8.1 无限序列
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 惰性数据流
把"读取 → 处理"管线拆成多个生成器,按需驱动:
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 即一个状态点:
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 在内部为生成器生成了专门的字节码路径。
性能开销主要在暂停/恢复时的上下文保存,通常远小于一次完整函数调用,但仍不适用于极端高频的拉取场景。
十一、注意事项与边界
- 一次性消费:生成器对象只能消费一次。多次迭代需要重新调用生成器函数。
- 不能作为构造函数:
new gen()抛出 TypeError。 - 箭头函数不能是生成器:
async () => yield 1是非法的,必须使用function*。 - 不要 yield 副作用:例如
yield fs.write(...),因为无法保证消费者何时拉取。 - 错误处理:生成器内部的
try/catch只能捕获同步错误;异步生成器中的await抛错需单独处理。 - 调试:调试器对生成器的支持因环境而异,必要时需在
yield处设置断点。
十二、版本与兼容性
| 版本 | 新增特性 |
|---|---|
| ES2015 (ES6) | 生成器函数、yield、yield* |
| ES2018 | 异步生成器 async function*、for await...of |
现代浏览器(Chrome 39+、Firefox 26+、Safari 10+)和 Node.js(4+)均已支持。
十三、总结
生成器函数提供的是"可暂停的同步执行模型 + 标准迭代器协议 + 双向通信通道"。判断何时使用:
- 惰性求值或无限序列:生成器天然合适。
- 表达状态机:每个
yield即一个状态点。 - 异步数据流:用异步生成器 +
for await...of。 - 同步风格的异步:优先使用
async/await,除非需要兼容老环境。
理解生成器的关键不在于记住语法,而在于把握"函数执行从一次性变成可中断"这一根本变化:函数不再只是"调用 → 返回值",而是一段可由外部控制的、可持续的状态机。
参考资料
- ECMAScript 2024 规范,§ 27.5 Generator Objects
- MDN: Iterators and generators
- Axel Rauschmayer, Exploring ES6
延伸阅读
- 从Promise到Generator和async&await — 从异步流程角度理解生成器在回调、Promise 和 async/await 演进中的角色
