javascript的生成器函数是什么_如何使用yield进行惰性求值与迭代【教程】

生成器函数是带暂停能力的函数,调用后返回实现迭代协议的Generator对象,代码在每次next()调用时执行到yield处暂停;yield让出控制权并返回值,yield*可委托迭代,支持惰性求值。

生成器函数本质是带暂停能力的函数

它不是普通函数,调用后不立即执行,而是返回一个 Generator 对象 ——

这个对象本身就是一个可迭代协议(Symbol.iterator)和迭代器协议(next() 方法)的实现体。关键在于:函数体内的代码只在每次调用 next() 时执行到下一个 yield 处并暂停。

yield 不是返回值,是“让出控制权”的指令

每次遇到 yield,函数暂停,把右侧表达式的值作为 value 返回,并将状态冻结;下次调用 next() 时从暂停处继续,且可接收传入的参数作为上一个 yield 表达式的返回值(注意:首次 next() 传参无效)。

  • yield 只能在生成器函数内使用,否则语法错误
  • 不能在箭头函数中使用 yield
  • yield 后面可以跟任意表达式,包括函数调用、Promise、甚至另一个 yield* 委托
  • 函数末尾隐式返回 { value: undefined, done: true }
function* countdown(n) {
  while (n > 0) {
    yield n;
    n--;
  }
}
const gen = countdown(3);
console.log(gen.next()); // { value: 3, done: false }
console.log(gen.next()); // { value: 2, done: false }
console.log(gen.next()); // { value: 1, done: false }
console.log(gen.next()); // { value: undefined, done: true }

yield* 委托迭代,避免手动遍历

当需要把另一个可迭代对象(数组、字符串、其他生成器等)的值“内联”进当前生成器时,yield* 比循环 + yield 更简洁、更符合语义,且自动处理 returnthrow 传递。

  • yield* iterable 会逐个 yield iterable 的每个值
  • 如果 iterable 是另一个生成器,它的 return 值会成为 yield* 表达式的返回值
  • 对数组、Set、Map 使用 yield* 是合法的,因为它们实现了迭代协议
function* numbers() {
  yield 1;
  yield* [2, 3];
  yield* "45";
}
Array.from(numbers()); // [1, 2, 3, '4', '5']

惰性求值的真实价值在“不提前计算”和“按需中断”

生成器常被误认为只是“写法花哨的数组”,但它真正不可替代的地方在于:数据源可能无限、开销巨大或依赖副作用(如 I/O),而你只需要前几个结果。

  • 无限序列(斐波那契、素数筛)不会卡死,只算你要的那几项
  • 读取大文件时,每 yield 一行,内存只存当前行,而非全部加载
  • 配合 for...of 或展开运算符 [...gen] 时,一旦外部逻辑 break 或异常退出,生成器内部未执行的代码就不会运行
  • 注意:Array.from(gen)[...gen] 仍会遍历全部,除非你手动控制 next()
function* fibonacci() {
  let [a, b] = [0, 1];
  while (true) {
    yield a;
    [a, b] = [b, a + b];
  }
}
// 只取前 5 项,后面完全不计算
const first5 = [];
const fib = fibonacci();
for (let i = 0; i < 5; i++) {
  first5.push(fib.next().value);
}

生成器的暂停/恢复机制底层依赖执行上下文栈的保存与还原,不是靠定时器或异步队列;这意味着它天然同步、零调度开销,但也意味着不能直接 await 一个 yield —— 需要 async function* 配合 yield 返回 Promise,再由消费者用 for await...of 消费。这点容易混淆,也最容易漏掉错误处理。