1. 生成器函数是什么?
第一次看到function*这种语法时,我完全摸不着头脑。直到在实际项目中遇到需要处理复杂异步流程的场景,才发现生成器函数这个ES6特性简直是个宝藏。简单来说,生成器函数是一种可以暂停和恢复执行的特殊函数,它通过yield关键字来控制执行流程。
与普通函数一次性执行完所有代码不同,生成器函数就像一个可以随时暂停的生产线。每次调用next()方法,生产线就运转到下一个yield位置停下,交出当前结果,等待下次继续。这种特性特别适合处理需要分步执行的任务,比如大数据分块处理、异步操作序列化等场景。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心语法与工作原理
2.1 基础语法结构
生成器函数的声明方式很特别,需要在function关键字后加一个星号:
javascript复制function* myGenerator() {
yield 'first';
yield 'second';
return 'finished';
}
调用生成器函数时,它不会立即执行函数体,而是返回一个迭代器对象:
javascript复制const gen = myGenerator();
console.log(gen.next()); // { value: 'first', done: false }
console.log(gen.next()); // { value: 'second', done: false }
console.log(gen.next()); // { value: 'finished', done: true }
2.2 执行流程解析
生成器函数的执行过程就像一场精心编排的芭蕾舞:
- 首次调用next()时,函数从开始执行到第一个yield处暂停
- 后续每次next()调用,函数从上个yield处恢复,执行到下一个yield
- 遇到return或函数结束时,done属性变为true
这个机制使得我们可以精确控制函数的执行进度,这在处理复杂异步逻辑时特别有用。
3. 实际应用场景
3.1 异步流程控制
在async/await出现之前,生成器函数配合Promise是实现异步代码同步化写法的利器。通过第三方库如co,可以这样写:
javascript复制function* fetchUser() {
const user = yield fetch('/api/user');
const posts = yield fetch(`/api/posts/${user.id}`);
return { user, posts };
}
// 使用co库执行
co(fetchUser).then(result => console.log(result));
虽然现在有了async/await,但理解这种模式对掌握JavaScript异步编程很有帮助。
3.2 无限序列生成
生成器非常适合生成无限序列,因为它是惰性求值的:
javascript复制function* idGenerator() {
let id = 1;
while(true) {
yield id++;
}
}
const ids = idGenerator();
console.log(ids.next().value); // 1
console.log(ids.next().value); // 2
// 可以无限调用...
3.3 状态机实现
用生成器实现状态机非常优雅:
javascript复制function* trafficLight() {
while(true) {
yield 'red';
yield 'yellow';
yield 'green';
yield 'yellow';
}
}
const light = trafficLight();
setInterval(() => {
console.log(light.next().value);
}, 1000);
4. 高级特性与技巧
4.1 双向通信
生成器函数支持双向通信,可以通过next()方法向生成器传递参数:
javascript复制function* calculator() {
const x = yield '请输入x';
const y = yield '请输入y';
return x + y;
}
const calc = calculator();
console.log(calc.next().value); // "请输入x"
console.log(calc.next(10).value); // "请输入y"
console.log(calc.next(20).value); // 30
4.2 错误处理
可以通过throw()方法向生成器内部抛出错误:
javascript复制function* errorDemo() {
try {
yield '正常执行';
} catch(e) {
yield `捕获错误: ${e}`;
}
}
const demo = errorDemo();
console.log(demo.next().value); // "正常执行"
console.log(demo.throw('测试错误').value); // "捕获错误: 测试错误"
4.3 组合生成器
可以使用yield*来委托给另一个生成器:
javascript复制function* generatorA() {
yield 'a1';
yield 'a2';
}
function* generatorB() {
yield 'b1';
yield* generatorA();
yield 'b2';
}
for (const val of generatorB()) {
console.log(val); // 依次输出: b1, a1, a2, b2
}
5. 常见问题与解决方案
5.1 生成器只能遍历一次
生成器产生的迭代器是一次性的,遍历完后就不能再次使用。如果需要多次遍历,应该重新调用生成器函数:
javascript复制function* numbers() {
yield 1;
yield 2;
}
const nums = numbers();
console.log([...nums]); // [1, 2]
console.log([...nums]); // [] (已经遍历完了)
5.2 yield的位置很重要
yield只能在生成器函数内部使用,在回调函数中使用会报错:
javascript复制function* wrongUse() {
[1,2,3].forEach(item => {
yield item; // 语法错误!
});
}
正确的做法是:
javascript复制function* correctUse() {
for (const item of [1,2,3]) {
yield item;
}
}
5.3 性能考量
虽然生成器很强大,但在性能敏感的场景要谨慎使用。创建生成器对象比普通函数调用开销大,在热代码路径中可能会成为瓶颈。
6. 现代JavaScript中的角色
随着async/await的普及,生成器函数在异步编程中的直接使用减少了。但它仍然是理解JavaScript异步机制的重要概念,并且在以下场景依然有价值:
- 实现自定义迭代器
- 处理大数据的分块处理
- 实现复杂的状态机
- 创建惰性计算的数据流
生成器函数展示了JavaScript语言的灵活性和表现力,是每个JS开发者都应该掌握的高级特性。虽然日常开发中可能不常直接使用,但理解它的工作原理能让你对JavaScript的执行模型有更深入的认识。
