在 JavaScript 中,生成器函数(Generator Function)

在 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)的底层原理。
相关推荐
Lethehong1 小时前
从安装到实测:基于 Claude Code + GLM-4.7 的前端生成与评测实战
前端
恋猫de小郭1 小时前
iOS + AI ,国外一个叫 Rork Max 的项目打算替换掉 Xcode
android·前端·flutter
宇木灵2 小时前
C语言基础-三、流程控制语句
java·c语言·前端
qq8406122332 小时前
Nodejs+vue基于elasticsearch的高校科研期刊信息管理系统_mb8od
前端·vue.js·elasticsearch
Quz3 小时前
QML与JavaScript 交互的四种方式
javascript·qt·交互
会周易的程序员3 小时前
cNetgate插件架构设计详解 动态库 脚本二开lua, python, javascript
javascript·c++·python·物联网·lua·iot
哆啦A梦15884 小时前
Vue3魔法手册 作者 张天禹 012_路由_(一)
前端·typescript·vue3
RaidenLiu5 小时前
别再手写 MethodChannel 了:Flutter Pigeon 工程级实践与架构设计
前端·flutter·前端框架
~央千澈~6 小时前
抖音弹幕游戏开发之第17集:添加日志系统·优雅草云桧·卓伊凡
linux·服务器·前端