在JavaScript的世界里,函数通常是一次性执行完毕的:从定义开始,到返回结束。但如果你希望一个函数能像“魔法盒”一样,在执行过程中随时暂停、恢复,甚至动态生成数据流,你会怎么做?答案就是生成器(Generator)。今天,我们就来揭开生成器的神秘面纱,看看它是如何通过function*
和yield
这两个“魔法咒语”改变我们编写代码的方式。
生成器是一种特殊的函数,它可以在执行过程中暂停并恢复。与普通函数不同,生成器不会一次性返回结果,而是按需逐步产出值。你可以把它想象成一个“流水线”:每次调用next()
方法,它就会从上次暂停的位置继续运行,直到下一个yield
或函数结束。
生成器函数使用function*
定义,并通过yield
关键字产出值:
function* simpleGenerator() {
yield 1;
yield 2;
yield 3;
}
const gen = simpleGenerator();
console.log(gen.next()); // { value: 1, done: false }
console.log(gen.next()); // { value: 2, done: false }
console.log(gen.next()); // { value: 3, done: false }
console.log(gen.next()); // { value: undefined, done: true }
yield
:暂停函数执行,并返回一个值。next()
:恢复执行,直到下一个yield
或函数结束。done
:布尔值,表示生成器是否执行完毕。生成器本身就是一个迭代器对象,这意味着它可以被for...of
循环直接遍历:
function* generateNumbers() {
yield 1;
yield 2;
yield 3;
}
for (let num of generateNumbers()) {
console.log(num); // 依次输出 1, 2, 3
}
生成器的核心在于它的状态机能力。每次调用next()
时,生成器会从上次暂停的位置继续执行,直到遇到下一个yield
或函数结束。这种机制让生成器能够像“分段式”函数一样工作。
function* cooking() {
console.log("Step 1: Prepare ingredients");
yield "Ingredients prepared";
console.log("Step 2: Start cooking");
yield "Cooking in progress";
console.log("Step 3: Serve the dish");
return "Dish is ready!";
}
const kitchen = cooking();
console.log(kitchen.next().value); // Step 1: ... -> "Ingredients prepared"
console.log(kitchen.next().value); // Step 2: ... -> "Cooking in progress"
console.log(kitchen.next().value); // Step 3: ... -> "Dish is ready!"
在这个例子中,生成器模拟了一个烹饪过程:每次调用next()
,程序会从上一个步骤继续执行,并返回当前阶段的状态。
yield
的双向通信生成器不仅能产出值,还能接收外部传入的值。通过next()
传递参数,可以实现与生成器的双向通信:
function* sumValues() {
const a = yield "Enter first value:";
const b = yield "Enter second value:";
return a + b;
}
const calculator = sumValues();
console.log(calculator.next().value); // "Enter first value:"
console.log(calculator.next(5).value); // "Enter second value:"
console.log(calculator.next(3).value); // 8
next()
启动生成器,但不会传入值(因为第一个yield
前没有赋值)。next(value)
时,传入的值会作为前一个yield
的返回值。生成器最大的优势在于惰性求值(Lazy Evaluation)。它不会一次性生成所有数据,而是按需生成,从而节省内存。例如,生成一个无限数列:
function* infiniteSequence() {
let i = 0;
while (true) {
yield i++;
}
}
const numbers = infiniteSequence();
console.log(numbers.next().value); // 0
console.log(numbers.next().value); // 1
console.log(numbers.next().value); // 2
虽然代码看似会无限运行,但由于生成器的惰性特性,它实际上只在需要时生成下一个值。
生成器可以与Promise
结合,实现更优雅的异步控制流。通过递归调用next()
,我们可以将异步操作写成同步风格:
function asyncOperation() {
return new Promise(resolve => setTimeout(resolve, 1000));
}
function* asyncFlow() {
console.log("Start");
yield asyncOperation();
console.log("Middle");
yield asyncOperation();
console.log("End");
}
const flow = asyncFlow();
function step() {
const result = flow.next();
if (!result.done) {
result.value.then(() => step());
}
}
step(); // 输出:Start → Middle → End,每个步骤间隔1秒
这种模式被称为协程(Coroutine),它让异步代码的逻辑更加清晰。
生成器可以轻松创建自定义的迭代器。例如,遍历一个对象的所有属性:
const myObject = { a: 1, b: 2, c: 3 };
myObject[Symbol.iterator] = function* () {
for (const key in this) {
yield [key, this[key]];
}
};
for (const [key, value] of myObject) {
console.log(`${key}: ${value}`); // a: 1, b: 2, c: 3
}
yield*
)yield*
允许一个生成器委托给另一个生成器,相当于“合并”多个生成器的产出:
function* gen1() {
yield "A";
yield "B";
}
function* gen2() {
yield "X";
yield* gen1(); // 委托给 gen1
yield "Y";
}
for (const value of gen2()) {
console.log(value); // X, A, B, Y
}
通过return()
和throw()
方法,可以强制结束生成器的执行:
function* cleanupExample() {
try {
yield "Start";
yield "Processing";
} finally {
yield "Cleaning up...";
}
}
const gen = cleanupExample();
gen.next(); // { value: "Start", done: false }
gen.return("Aborted"); // { value: "Aborted", done: true }
// 此时,finally块中的代码会被执行("Cleaning up..."),但不会再次产出值。
yield
点。Promise
链。生成器是JavaScript中一个强大但常被低估的特性。它通过function*
和yield
赋予了函数“暂停与恢复”的能力,让开发者能够以更优雅的方式处理数据流和异步操作。无论是简化异步代码、优化内存使用,还是构建复杂的控制流,生成器都能成为你的“瑞士军刀”。
下次当你需要一个“能暂停的函数”时,不妨试试生成器——它或许会带来意想不到的惊喜!
参考资料: