在 JavaScript 中,生成器函数(Generator Function) 是一种特殊的函数,它允许你暂停和恢复代码的执行。这与传统的函数"一运行到底"的模式完全不同。
我们可以从以下几个维度来深入了解它:
1. 语法特征
生成器函数通过在 function 关键字后面加一个星号 * 来定义,并在内部使用 yield 关键字来暂停执行。
javascript
function* myGenerator() {
console.log("开始执行");
yield "第一站";
console.log("继续执行");
yield "第二站";
return "终点";
}
2. 核心机制:它是如何工作的?
当你调用一个生成器函数时,它不会立即执行 函数体内的代码,而是返回一个特殊的迭代器对象(Generator Object)。
你需要通过调用这个对象的 .next() 方法来控制函数的执行:
- 第一次调用
.next():函数开始执行,直到遇到第一个yield。它会暂停,并返回一个对象{ value: "yield后的值", done: false }。 - 再次调用
.next():函数从上次暂停的地方继续往后走,直到遇到下一个yield。 - 直到遇到
return或执行完毕 :返回{ value: "返回值", done: true },此时生成器宣告完成。
3. 代码演示
lua
const gen = myGenerator(); // 此时代码一行都没跑,只是拿到了一个控制器
console.log(gen.next());
// 输出: 开始执行
// 返回: { value: '第一站', done: false }
console.log(gen.next());
// 输出: 继续执行
// 返回: { value: '第二站', done: false }
console.log(gen.next());
// 返回: { value: '终点', done: true }
4. 为什么需要生成器?(使用场景)
# A. 惰性求值(节省内存)
如果你需要一个无限的序列(比如斐波那契数列),你不需要一次性把它们全存在数组里(那会撑爆内存),而是用生成器"要一个给一个"。
ini
function* fibonacci() {
let [prev, curr] = [0, 1];
while (true) {
yield curr;
[prev, curr] = [curr, prev + curr];
}
}
# B. 可迭代协议
生成器返回的对象默认实现了迭代器协议,所以你可以直接用 for...of 循环它:
csharp
for (let value of myGenerator()) {
console.log(value); // 会自动依次打印 yield 出来的值
}
# C. 异步编程的基石
在 async/await 普及之前,生成器配合 yield 是处理异步回调地狱的主流方案(比如著名的 co 库)。它能让你用同步的写法写异步代码。虽然现在大多使用 async/await,但 async/await 的底层原理正是生成器 + Promise。
总结
- 普通函数:一旦开始,不可停止,直到结束。
- 生成器函数 :像一个可以随时按"暂停键"的播放器,由外部(
.next())来决定什么时候播下一段。
如果你正在处理大量数据计算,或者需要编写复杂的迭代逻辑,生成器是一个非常优雅的工具。
在 JavaScript 生成器中,除了常用的 .next(),还有两个非常重要的原型方法:.return() 和 .throw()。它们允许你从外部更精细地控制生成器内部的状态。
1. generator.return(value):强制终结
.return() 方法会立即结束生成器,就像在生成器内部执行了一行 return 语句一样。
- 效果 :生成器直接进入"完成"状态(
done: true)。 - 返回值 :返回
{ value: 你传入的值, done: true }。
# 代码演示:
javascript
function* numbers() {
yield 1;
yield 2;
yield 3;
}
const gen = numbers();
console.log(gen.next()); // { value: 1, done: false }
// 提前终结
console.log(gen.return('手动结束')); // { value: '手动结束', done: true }
console.log(gen.next()); // { value: undefined, done: true }
# 特殊情况:try...finally
如果生成器内部有 try...finally 块,即使外部调用了 .return(),finally 块里的代码依然会执行,执行完后再彻底终结。
javascript
function* demo() {
try {
yield "运行中";
} finally {
console.log("清理工作已完成");
}
}
const g = demo();
g.next();
g.return(); // 控制台会先打印 "清理工作已完成",然后返回 { value: undefined, done: true }
2. generator.throw(error):注入错误
.throw() 极其强大,它允许你从外部向生成器内部抛出一个异常 。这个错误会在生成器当前暂停的 yield 表达式位置被"引爆"。
- 效果 :如果在内部被捕获(
try...catch),生成器可以继续运行;如果未被捕获,生成器会异常关闭。
# 场景 A:内部捕获错误(继续运行)
这是生成器处理异步错误最优雅的地方,它可以让异步错误看起来像同步错误一样被 catch。
javascript
function* genTask() {
try {
yield "第一步";
} catch (e) {
console.log("内部捕获到了错误:", e);
}
yield "第二步";
}
const g = genTask();
console.log(g.next().value); // "第一步"
// 外部注入错误
console.log(g.throw("出Bug了").value);
// 输出: 内部捕获到了错误: 出Bug了
// 返回: "第二步" (因为 catch 之后函数继续运行到了下一个 yield)
# 场景 B:内部未捕获(生成器死亡)
如果生成器内部没有 try...catch,错误会冒泡到外部,且生成器状态变为 done: true。
3. 三个方法的横向对比
这三个方法本质上都是在向生成器"发送指令",只是目的不同:
方法
外部操作
内部对应行为
最终状态
.next(val)
传递值
让 yield 表达式返回 val
继续运行或完成
.return(val)
强制停止
在当前位置执行 return val
立即完成
.throw(err)
注入错误
在当前位置抛出 throw err
捕获则继续,否则崩溃
总结建议
.return()常用于资源清理,比如你不再需要一个无限序列时,手动关闭它。.throw()是异步流程控制的核心,配合Promise使用时,它是async/await能够捕获异步错误(try...catch)的底层原理。