JavaScript 生成器函数
什么是生成器函数?
生成器函数是ES6(ECMAScript 2015)引入的一种特殊函数,它可以在执行过程中被暂停和恢复,这使得它成为处理异步操作的强大工具。与普通函数不同,生成器函数执行后会返回一个迭代器对象,我们可以通过这个迭代器控制函数的执行流程。
生成器函数在语法上使用星号(*
)来标识,例如function* myGenerator() {}
,并且在函数体内部使用yield
关键字来暂停执行并产生值。
生成器函数 = 可暂停和恢复的函数 + 内部迭代器
基本语法
function* simpleGenerator() {
console.log('开始执行');
yield 1;
console.log('第一次暂停后继续');
yield 2;
console.log('第二次暂停后继续');
yield 3;
console.log('生成器函数结束');
}
// 创建一个生成器对象
const generator = simpleGenerator();
// 使用next()方法执行生成器,直到遇到yield
console.log(generator.next()); // 打印"开始执行",然后返回{value: 1, done: false}
console.log(generator.next()); // 打印"第一次暂停后继续",然后返回{value: 2, done: false}
console.log(generator.next()); // 打印"第二次暂停后继续",然后返回{value: 3, done: false}
console.log(generator.next()); // 打印"生成器函数结束",然后返回{value: undefined, done: true}
输出结果:
开始执行
{value: 1, done: false}
第一次暂停后继续
{value: 2, done: false}
第二次暂停后继续
{value: 3, done: false}
生成器函数结束
{value: undefined, done: true}
生成器的工作原理
要理解生成器函数,我们可以将其工作原理可视化:
生成器函数的关键点:
- 调用生成器函数不会立即执行函数体,而是返回一个生成器对象
- 每次调用生成器对象的
next()
方法,函数会执行到下一个yield
表达式处 yield
表达式的值会被作为返回对象的value
属性值- 当没有更多
yield
表达式或执行到return
语句时,done
属性变为true
yield表达式的双向通信
yield
不仅可以向外传递值,还可以接收外部传入的值,实现双向通信:
function* communicator() {
const x = yield '请输入第一个值';
console.log('收到第一个值:', x);
const y = yield '请输入第二个值';
console.log('收到第二个值:', y);
return x + y;
}
const comm = communicator();
// 第一次调用next(),启动生成器直到第一个yield
console.log(comm.next()); // {value: '请输入第一个值', done: false}
// 第二次调用next()并传入值,这个值会作为第一个yield表达式的结果
console.log(comm.next(10)); // 打印"收到第一个值:10",然后返回{value: '请输入第二个值', done: false}
// 第三次调用next()并传入另一个值
console.log(comm.next(20)); // 打印"收到第二个值:20",然后返回{value: 30, done: true}
输出结果:
{value: '请输入第一个值', done: false}
收到第一个值:10
{value: '请输入第二个值', done: false}
收到第二个值:20
{value: 30, done: true}
第一次调用next()
方法时传入的参数会被忽略,因为此时还没有一个处于暂停状态的yield
表达式来接收这个值。
使用for...of迭代生成器
生成器对象是可迭代的,因此可以使用for...of
循环来遍历它产生的所有值:
function* countToFive() {
yield 1;
yield 2;
yield 3;
yield 4;
yield 5;
}
for (const num of countToFive()) {
console.log(num);
}
输出结果:
1
2
3
4
5
实际应用场景
1. 生成无限序列
生成器可以创建无限序列,只在需要时才计算下一个值:
function* infiniteFibonacci() {
let a = 0, b = 1;
while (true) {
yield a;
[a, b] = [b, a + b];
}
}
const fib = infiniteFibonacci();
// 只获取需要的斐波那契数
for (let i = 0; i < 10; i++) {
console.log(fib.next().value);
}
输出结果:
0
1
1
2
3
5
8
13
21
34
2. 简化异步代码
生成器可以与Promise结合,简化异步代码:
// 一个模拟API调用的函数
function fetchData(url) {
return new Promise(resolve => {
setTimeout(() => resolve(`来自${url}的数据`), 1000);
});
}
// 一个简单的生成器运行器
function run(generatorFunction) {
const generator = generatorFunction();
function handle(result) {
if (result.done) return Promise.resolve(result.value);
return Promise.resolve(result.value)
.then(res => handle(generator.next(res)))
.catch(err => handle(generator.throw(err)));
}
return handle(generator.next());
}
// 使用生成器编写异步代码,看起来像同步代码
function* main() {
try {
const data1 = yield fetchData('/api/data1');
console.log(data1);
const data2 = yield fetchData('/api/data2');
console.log(data2);
return '所有数据加载完成';
} catch (error) {
console.error('出错了:', error);
}
}
// 运行生成器
run(main).then(result => console.log(result));
输出结果(每行相隔约1秒):
来自/api/data1的数据
来自/api/data2的数据
所有数据加载完成
这种使用生成器处理异步操作的模式,是后来async/await语法的前身。事实上,async/await可以看作是生成器和Promise的语法糖。
3. 实现数据流处理
生成器非常适合处理大型数据集或流式数据:
function* processDataStream(dataArray) {
for (const item of dataArray) {
// 这里可以对每个数据项进行处理
yield item.toUpperCase();
}
}
// 使用生成器处理数据
const data = ['hello', 'world', 'javascript', 'generators'];
const processor = processDataStream(data);
for (const processedItem of processor) {
console.log(processedItem);
}
输出结果:
HELLO
WORLD
JAVASCRIPT
GENERATORS
生成器委托
通过yield*
表达式,一个生成器可以委托给另一个生成器:
function* gen1() {
yield 'a';
yield 'b';
}
function* gen2() {
yield 1;
yield* gen1(); // 委托给gen1
yield 2;
}
for (const item of gen2()) {
console.log(item);
}
输出结果:
1
a
b
2
错误处理
生成器提供了强大的错误处理机制:
function* errorDemo() {
try {
yield 'Start';
yield 'Middle';
yield 'End';
} catch (e) {
console.error('捕获到错误:', e);
yield '错误处理成功';
}
}
const g = errorDemo();
console.log(g.next().value); // 'Start'
console.log(g.next().value); // 'Middle'
// 向生成器抛出错误
console.log(g.throw(new Error('测试错误')).value); // 捕获到错误: Error: 测试错误 / '错误处理成功'
输出结果:
Start
Middle
捕获到错误: Error: 测试错误
错误处理成功
总结
JavaScript生成器函数是一种强大的语言特性,为我们提供了:
- 控制函数执行流程的能力
- 按需生成数据的方式
- 简化异步编程的模式
- 处理大型数据集的优雅解决方案
生成器函数是JavaScript异步编程的重要工具,虽然async/await语法在某些场景下可能更简洁,但生成器提供的灵活性使其在很多复杂情况下仍然不可替代。
练习题
- 创建一个生成器函数,生成前n个偶数
- 实现一个使用生成器的"范围"函数,类似于Python中的range()
- 创建一个生成器,读取用户输入并对每个输入进行处理
- 使用生成器模拟异步任务队列
延伸阅读
通过掌握生成器函数,你将能够编写更简洁、更高效的异步代码,并能够解决许多传统回调方式难以处理的复杂问题。