你是怎么理解ES6中Generator的?使用场景?
掌握 Generator 的 yield、next、throw 和委托迭代,区分流程控制能力与 async/await 的语言语义。
你是怎么理解ES6中Generator的?使用场景?
问题解析
Generator是ES6引入的一种特殊的函数,它是异步编程的重要解决方案之一。这个问题考察对Generator函数的本质理解(状态机、迭代器)、yield表达式的工作机制、与Promise和async/await的关系,以及实际应用场景。
核心概念
1. Generator函数的本质
- 状态机:封装了多个内部状态的函数
- 迭代器生成器:返回符合Iterator接口的对象
- 可暂停执行:通过yield暂停,通过next()恢复
2. 核心特征
function*声明语法yield表达式暂停执行并返回数据next()方法恢复执行并可传入参数- 返回
{ value, done }对象
3. Generator与异步编程
- 提供同步化的异步代码书写方式
- 可以配合执行器解释 async/await 的控制流,也可用于转译实现
- 与Promise配合实现流程控制
详细解答
Generator函数基本语法
// 1. 声明方式
function* gen() {
yield 1;
yield 2;
return 3;
}
// 2. 调用返回迭代器对象,不立即执行
const g = gen();
console.log(g); // Generator { }
// 3. next()方法执行到下一个yield
console.log(g.next()); // { value: 1, done: false }
console.log(g.next()); // { value: 2, done: false }
console.log(g.next()); // { value: 3, done: true }
console.log(g.next()); // { value: undefined, done: true }
yield表达式详解
function* gen() {
yield 'first';
console.log('继续执行');
yield 'second';
console.log('再次继续');
yield 'third';
}
const g = gen();
g.next(); // 输出: 无,返回 { value: 'first', done: false }
// 暂停在第一个yield后
g.next(); // 输出: '继续执行',返回 { value: 'second', done: false }
g.next(); // 输出: '再次继续',返回 { value: 'third', done: false }
g.next(); // 返回 { value: undefined, done: true }
// yield的返回值
function* genWithReturn() {
const received = yield 'sent';
console.log('received:', received);
yield received;
}
const g2 = genWithReturn();
g2.next(); // { value: 'sent', done: false }
g2.next('hello'); // 输出: 'received: hello',返回 { value: 'hello', done: false }
yield接收next()传入的参数
function* gen() {
const a = yield '第一步';
console.log('a =', a); // 11
const b = yield '第二步';
console.log('b =', b); // 22
const c = yield '第三步';
console.log('c =', c); // 33
return a + b + c;
}
const g = gen();
// 第一个next()的传参会被忽略,因为用于启动生成器
console.log(g.next()); // { value: '第一步', done: false }
console.log(g.next(11)); // { value: '第二步', done: false }
console.log(g.next(22)); // { value: '第三步', done: false }
console.log(g.next(33)); // { value: 66, done: true }
for...of遍历Generator
function* gen() {
yield 1;
yield 2;
yield 3;
return 4; // for...of不会遍历return的值
}
for (const value of gen()) {
console.log(value); // 1, 2, 3
}
// 使用展开运算符
const arr = [...gen()]; // [1, 2, 3]
// 解构赋值
const [a, b, c] = gen(); // a=1, b=2, c=3
为对象部署Iterator接口
// 使用Generator为对象添加遍历能力
const obj = {
data: ['a', 'b', 'c'],
*[Symbol.iterator]() {
for (const item of this.data) {
yield item;
}
}
};
for (const item of obj) {
console.log(item); // 'a', 'b', 'c'
}
// 或者遍历对象的属性
const obj2 = {
name: 'Tom',
age: 25,
job: 'developer',
*[Symbol.iterator]() {
const keys = Object.keys(this);
for (const key of keys) {
yield [key, this[key]];
}
}
};
for (const [key, value] of obj2) {
console.log(key, value);
}
Generator与Promise结合
把 Promise 交给 yield 之后,生成器会暂停。外部执行器订阅它:成功时用 next(value) 送回结果,失败时用 throw(error) 在暂停点抛出异常;生成器内部才可以按书写顺序继续,或用 try/catch 处理失败。这个过程没有把异步操作变成同步阻塞。
完整实现统一维护在 协程与异步控制:执行器始终返回 Promise,吸收普通值和 thenable,处理同步异常,并传递最终 return 的结果。本篇保留语法和应用场景,不再维护第二套执行器。
Generator的错误处理
function* gen() {
try {
yield 1;
yield 2;
yield 3;
} catch (e) {
console.log('内部捕获:', e);
yield '错误已处理';
}
}
const g = gen();
g.next(); // { value: 1, done: false }
g.throw('错误'); // 内部捕获: 错误,返回 { value: '错误已处理', done: false }
g.next(); // { value: undefined, done: true }
// throw 会恢复执行;未被内部捕获时向调用方抛出并关闭生成器
function* gen2() {
yield 1;
yield 2;
}
const g2 = gen2();
g2.next(); // { value: 1, done: false }
try {
g2.throw('错误'); // 未被捕获,抛出到外部
} catch (e) {
console.log('外部捕获:', e);
}
深入理解
1. Generator的状态机本质
Generator函数可以看作是一个状态机,封装了多个内部状态:
function* stateMachine() {
// 状态1
yield 'STATE_1';
// 状态2
yield 'STATE_2';
// 状态3
yield 'STATE_3';
// 最终状态
return 'FINAL';
}
// 每次yield对应一个状态转换
// 这种特性使得Generator非常适合实现工作流、状态流转等场景
2. Generator与协程
Generator实现了JavaScript中的"半协程"(semi-coroutine):
// 协程特点:可以暂停和恢复,但只能由调用方恢复
// 对比线程:线程是抢占式多任务,协程是协作式多任务
function* taskA() {
console.log('Task A - Step 1');
yield; // 让出执行权
console.log('Task A - Step 2');
yield;
console.log('Task A - Step 3');
}
function* taskB() {
console.log('Task B - Step 1');
yield;
console.log('Task B - Step 2');
yield;
console.log('Task B - Step 3');
}
// 协作式调度
const a = taskA();
const b = taskB();
a.next(); // Task A - Step 1
b.next(); // Task B - Step 1
a.next(); // Task A - Step 2
b.next(); // Task B - Step 2
// ...
3. Generator与async/await的关系
可以用「Generator + Promise + 自动执行器」理解 async/await 如何暂停、等待和恢复,转译工具也可以采用这个思路。但原生 async 是独立的语言特性,规范不要求引擎创建 Generator;不能把两者当成完全等价的实现。
| 对比 | 同步 Generator | 原生 async 函数 |
|---|---|---|
| 调用结果 | 生成器对象 | 一个新的 Promise |
| 何时开始函数体 | 第一次 next() 时 |
调用时立即运行,到 await 才暂停 |
| 谁恢复执行 | 调用方或自行编写的执行器 | 语言运行时在等待值敲定后调度恢复 |
| 中间值 | yield 原样交出值,不自动等待 Promise |
await 接纳 Promise、thenable 和普通值,恢复通过微任务完成 |
| 对外控制 | 暴露 next/throw/return |
不对调用者暴露这些迭代方法 |
日常依赖 Promise 的顺序流程优先使用 async/await;需要把暂停点和控制权交给调度器、实现惰性序列或状态机时仍可使用 Generator。具体语义见 async/await 的理解,执行器的异常与清理边界见 协程与异步控制。
4. yield* 委托迭代
// yield* 将迭代委托给另一个Generator或可迭代对象
function* gen1() {
yield 'a';
yield 'b';
}
function* gen2() {
yield 1;
yield* gen1(); // 委托给gen1
yield 2;
yield* [3, 4, 5]; // 委托给数组
yield 6;
}
const arr = [...gen2()]; // [1, 'a', 'b', 2, 3, 4, 5, 6]
// 获取被委托Generator的返回值
function* gen3() {
yield 'start';
const result = yield* gen4();
console.log('gen4 returned:', result);
yield 'end';
}
function* gen4() {
yield 'inner';
return 'done'; // 这个返回值会被yield*捕获
}
const g = gen3();
g.next(); // { value: 'start', done: false }
g.next(); // { value: 'inner', done: false }
g.next(); // 输出 gen4 returned: done,返回 { value: 'end', done: false }
5. Generator的内存特性
// Generator是惰性求值的,适合处理大数据
function* infiniteSequence() {
let i = 0;
while (true) {
yield i++;
}
}
// 不会占用大量内存,按需生成
const seq = infiniteSequence();
console.log(seq.next().value); // 0
console.log(seq.next().value); // 1
console.log(seq.next().value); // 2
// ...
// 对比数组实现
function infiniteArray() {
const arr = [];
for (let i = 0; i < 1000000; i++) {
arr.push(i);
}
return arr; // 内存占用大
}
最佳实践
1. 异步流程控制
// 使用co库或自行实现自动执行器
const co = require('co');
co(function* () {
const result = yield Promise.resolve(true);
return result;
}).then(value => {
console.log(value);
}, err => {
console.error(err.stack);
});
// 或者使用async/await(推荐)
async function modernAsync() {
const result = await Promise.resolve(true);
return result;
}
2. 数据流处理
// 使用Generator实现管道处理
function* map(iterable, fn) {
for (const item of iterable) {
yield fn(item);
}
}
function* filter(iterable, predicate) {
for (const item of iterable) {
if (predicate(item)) {
yield item;
}
}
}
function* take(iterable, n) {
let count = 0;
for (const item of iterable) {
if (count >= n) return;
yield item;
count++;
}
}
// 惰性求值的管道
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const result = [
...take(
filter(
map(numbers, x => x * x),
x => x % 2 === 0
),
3
)
];
// [4, 16, 36]
3. 实现可迭代对象
// 为二叉树实现遍历
class BinaryTree {
constructor(value, left = null, right = null) {
this.value = value;
this.left = left;
this.right = right;
}
// 中序遍历
*[Symbol.iterator]() {
if (this.left) {
yield* this.left;
}
yield this.value;
if (this.right) {
yield* this.right;
}
}
// 先序遍历
*preOrder() {
yield this.value;
if (this.left) {
yield* this.left.preOrder();
}
if (this.right) {
yield* this.right.preOrder();
}
}
}
const tree = new BinaryTree(
4,
new BinaryTree(2, new BinaryTree(1), new BinaryTree(3)),
new BinaryTree(6, new BinaryTree(5), new BinaryTree(7))
);
console.log([...tree]); // [1, 2, 3, 4, 5, 6, 7]
console.log([...tree.preOrder()]); // [4, 2, 1, 3, 6, 5, 7]
面试要点
-
Generator的本质:(深入阅读:1. Generator函数的本质)
- 状态机,封装多个内部状态
- 返回Iterator对象
- 可暂停和恢复执行的函数
-
核心语法:(深入阅读:2. 核心特征)
function*声明yield暂停并返回数据next()恢复执行,可传参- 返回
{ value, done }对象
-
next()传参机制:(深入阅读:yield接收next()传入的参数)
- 第一个next()的参数会被忽略
- 后续next(arg)的参数会成为上一个yield的返回值
-
for...of与Generator:(深入阅读:for...of遍历Generator)
- for...of会自动遍历Generator直到done为true
- 不会遍历return的值
- 可用于为对象部署Iterator接口
-
与Promise/async的关系:(深入阅读:3. Generator与async/await的关系)
- Generator 配合执行器可用于理解或转译 async 流程
- 原生 async 有独立语义,不要求由 Generator 实现
- 现代代码优先使用async/await
-
yield*的作用:(深入阅读:4. yield* 委托迭代)
- 委托迭代给另一个Generator或可迭代对象
- 可以获取被委托Generator的return值
-
使用场景:(深入阅读:详细解答)
- 异步操作的顺序表达(现代业务代码通常优先使用 async/await)
- redux-saga等中间件
- 为对象部署Iterator接口
- 惰性求值、无限序列
- 协程风格的任务调度
-
注意事项:(深入阅读:详细解答)
- Generator函数不能用作构造函数
- throw()方法会抛出错误到Generator内部
- return() 请求提前结束;finally 若继续 yield,仍需恢复以完成清理
来源与取舍
- 《JS异步编程.pdf》(FE-005)第 7-9 页:基本语法、暂停恢复与双向通信复用本篇现有小节,自动执行器统一链接到 JavaScript 的 Iterator/Generator 专题。
- 将来源中「async 就是 Generator 的语法糖」改为控制流模型与转译思路,明确原生语义边界;顺带修正
throw()被描述为调用next()、yield*示例漏掉内层产出值的说明。