跳到主要内容

JavaScript 生成器

什么是生成器?

生成器(Generator)是ES6引入的一项革命性特性,它允许我们创建可以暂停执行和恢复的函数。与普通函数不同,生成器函数执行时不会一次性运行到结束,而是可以在特定位置暂停,稍后再从暂停的地方继续执行。

小提示

生成器为JavaScript带来了一种全新的编程范式,特别适合处理异步流程和迭代大型数据集。

生成器函数的基础语法

生成器函数使用特殊的语法定义,在function关键字后面加上星号(*):

js
function* myGenerator() {
yield 'Hello';
yield 'World';
return 'End';
}

const gen = myGenerator();

关键点解析

  1. function* 声明了一个生成器函数
  2. yield 关键字用于暂停函数执行并返回一个值
  3. 调用生成器函数不会执行函数体,而是返回一个生成器对象

生成器的基本使用

生成器对象可以通过next()方法逐步执行:

js
function* countToThree() {
yield 1;
yield 2;
yield 3;
}

const generator = countToThree();

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 }

next()方法返回一个包含两个属性的对象:

  • value: yield表达式产生的值
  • done: 一个布尔值,表示生成器是否已完成执行

yield表达式的工作原理

yield是生成器函数的核心,它有两个作用:

  1. 暂停生成器函数的执行
  2. 向调用者返回一个值
js
function* demonstrateYield() {
console.log('开始执行');
yield '第一次暂停';
console.log('继续执行');
yield '第二次暂停';
console.log('最后部分');
return '完成';
}

const gen = demonstrateYield();

// 什么都不会打印,因为函数体还没有开始执行
console.log('第一次调用next()');
console.log(gen.next());
/* 输出:
开始执行
第一次调用next()
{ value: '第一次暂停', done: false }
*/

console.log('第二次调用next()');
console.log(gen.next());
/* 输出:
继续执行
第二次调用next()
{ value: '第二次暂停', done: false }
*/

console.log('第三次调用next()');
console.log(gen.next());
/* 输出:
最后部分
第三次调用next()
{ value: '完成', done: true }
*/

向生成器传递值

next()方法可以接收一个参数,该参数会作为上一个yield表达式的结果:

js
function* interactiveGenerator() {
const x = yield '请输入第一个数字';
const y = yield '请输入第二个数字';
return x + y;
}

const gen = interactiveGenerator();

console.log(gen.next()); // { value: '请输入第一个数字', done: false }
console.log(gen.next(5)); // { value: '请输入第二个数字', done: false }
console.log(gen.next(3)); // { value: 8, done: true }
注意

第一个next()调用的参数会被忽略,因为此时没有等待接收值的yield表达式。

生成器迭代

生成器对象是可迭代的,可以在for...of循环中使用:

js
function* fibonacci() {
let [prev, curr] = [0, 1];
while (true) {
yield curr;
[prev, curr] = [curr, prev + curr];
}
}

// 获取斐波那契数列的前10个数
const fib = fibonacci();
for (let i = 0; i < 10; i++) {
console.log(fib.next().value);
}
// 输出: 1, 1, 2, 3, 5, 8, 13, 21, 34, 55

更简洁的方式:

js
function* take(iterable, limit) {
for (const item of iterable) {
if (limit <= 0) return;
yield item;
limit--;
}
}

// 获取斐波那契数列的前10个数
for (const num of take(fibonacci(), 10)) {
console.log(num);
}

生成器的错误处理

生成器提供了throw()方法,可以在暂停的生成器内部抛出异常:

js
function* errorHandlingGenerator() {
try {
yield 'normal execution';
yield 'will not reach here if error is thrown';
} catch (e) {
yield `捕获到错误: ${e}`;
}
yield '继续执行';
}

const gen = errorHandlingGenerator();
console.log(gen.next()); // { value: 'normal execution', done: false }
console.log(gen.throw('自定义错误')); // { value: '捕获到错误: 自定义错误', done: false }
console.log(gen.next()); // { value: '继续执行', done: false }
console.log(gen.next()); // { value: undefined, done: true }

提前终止生成器

可以使用return()方法提前终止生成器的执行:

js
function* countToFive() {
for (let i = 1; i <= 5; i++) {
yield i;
}
}

const counter = countToFive();
console.log(counter.next()); // { value: 1, done: false }
console.log(counter.next()); // { value: 2, done: false }
console.log(counter.return('提前结束')); // { value: '提前结束', done: true }
console.log(counter.next()); // { value: undefined, done: true }

生成器委托

使用yield*表达式可以将控制权委托给另一个生成器:

js
function* generateNumbers() {
yield 1;
yield 2;
}

function* generateLetters() {
yield 'a';
yield 'b';
}

function* generateAll() {
yield* generateNumbers();
yield* generateLetters();
yield '完成';
}

const gen = generateAll();
console.log(gen.next()); // { value: 1, done: false }
console.log(gen.next()); // { value: 2, done: false }
console.log(gen.next()); // { value: 'a', done: false }
console.log(gen.next()); // { value: 'b', done: false }
console.log(gen.next()); // { value: '完成', done: false }
console.log(gen.next()); // { value: undefined, done: true }

实际应用场景

1. 惰性计算和无限序列

生成器可以表示无限序列,只在需要时计算值:

js
function* naturalNumbers() {
let n = 1;
while (true) {
yield n++;
}
}

// 获取前5个自然数的平方
const numbers = naturalNumbers();
const squares = Array.from({ length: 5 }, () => {
const num = numbers.next().value;
return num * num;
});

console.log(squares); // [1, 4, 9, 16, 25]

2. 简化异步操作

生成器可以与Promise结合使用,简化异步代码:

js
function fetchData(url) {
return fetch(url).then(response => response.json());
}

function* fetchUserData(userId) {
try {
const user = yield fetchData(`/api/users/${userId}`);
const posts = yield fetchData(`/api/posts?userId=${userId}`);
return { user, posts };
} catch (error) {
console.error('数据获取失败:', error);
}
}

// 执行生成器
function runGenerator(generator, ...args) {
const gen = generator(...args);

function handle(result) {
if (result.done) return Promise.resolve(result.value);

return Promise.resolve(result.value)
.then(res => handle(gen.next(res)))
.catch(err => handle(gen.throw(err)));
}

return handle(gen.next());
}

// 使用
runGenerator(fetchUserData, 1)
.then(data => console.log('用户数据:', data))
.catch(err => console.error('错误:', err));
备注

这种模式是async/await的前身,现代JavaScript通常直接使用async/await,但理解生成器有助于理解async/await的工作原理。

3. 状态机实现

生成器非常适合实现状态机:

js
function* trafficLightFSM() {
while (true) {
yield '红灯';
yield '黄灯';
yield '绿灯';
}
}

const trafficLight = trafficLightFSM();
console.log(trafficLight.next().value); // 红灯
console.log(trafficLight.next().value); // 黄灯
console.log(trafficLight.next().value); // 绿灯
console.log(trafficLight.next().value); // 红灯(循环)

4. 数据流处理

生成器可以用于处理大型数据流,按需处理数据:

js
function* processLargeFile(file) {
// 假设我们能按行读取文件
let line;
while ((line = file.readLine()) !== null) {
yield line;
}
}

function* filterLines(lines, keyword) {
for (const line of lines) {
if (line.includes(keyword)) {
yield line;
}
}
}

// 使用示例(伪代码)
const file = openLargeFile('data.txt');
const allLines = processLargeFile(file);
const filteredLines = filterLines(allLines, 'important');

for (const line of filteredLines) {
console.log(line);
}

生成器与迭代器协议

生成器实现了迭代器协议,这使得它们可以与其他迭代相关的特性无缝配合:

js
function* myGenerator() {
yield* [1, 2, 3];
yield* 'abc';
}

const gen = myGenerator();

// 使用扩展运算符
console.log([...gen]); // [1, 2, 3, 'a', 'b', 'c']

// 或者使用解构赋值
const gen2 = myGenerator();
const [a, b, ...rest] = gen2;
console.log(a, b, rest); // 1 2 [3, 'a', 'b', 'c']

总结

JavaScript生成器是一种强大的语言特性,它通过function*语法和yield关键字提供了函数执行暂停和恢复的能力。主要优点包括:

  1. 懒惰求值 - 只在需要时计算值,适合处理大型或无限数据集
  2. 双向通信 - 通过next()yield在调用者和生成器之间进行数据传递
  3. 简化异步编程 - 提供了比回调更直观的异步编程模型
  4. 资源管理 - 可以按需生成和处理数据,减少内存使用

生成器是掌握现代JavaScript的重要部分,尤其在处理复杂数据流和异步编程时非常有用。

练习与进一步探索

  1. 创建一个生成器函数,生成斐波那契数列的前20个数
  2. 实现一个树结构的深度优先遍历生成器
  3. 使用生成器实现一个简单的任务调度器
  4. 研究生成器与async/await的关系
  5. 尝试使用生成器简化一个复杂的异步操作流程

掌握生成器将为你打开JavaScript编程的新视角,帮助你写出更简洁、更强大的代码!