生成器函数与 yield:掌控函数执行的 “暂停键”

生成器函数是 ES6 里很实用的特性,而 yield 就像给函数装了个 “暂停开关”—— 能让函数执行到一半停下来,需要的时候再接着跑。这种 “可控的执行流程”,在处理异步操作、遍历大数据、实现状态切换时特别好用。

一、生成器:能 “中场休息” 的特殊函数

1.1 什么是生成器函数?

生成器函数是可以暂停执行、后续恢复的特殊函数,它的语法很容易识别:用 function* 定义,内部用 yield 控制暂停。

举个最简单的例子,感受下它的 “暂停 - 恢复” 能力:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 用 function* 定义生成器函数
function* stepGenerator() {
yield '第一步:准备';
yield '第二步:执行';
return '第三步:完成'; // return 的值会作为最后一次 next() 的 value
}

// 调用生成器函数:不会立即执行,而是返回一个“生成器对象”(类似控制器)
const generator = stepGenerator();

// 调用 next() 恢复执行,直到遇到下一个 yield 或 return
console.log(generator.next().value); // 输出“第一步:准备”(执行到第一个 yield 暂停)
console.log(generator.next().value); // 输出“第二步:执行”(恢复后到第二个 yield 暂停)
console.log(generator.next().value); // 输出“第三步:完成”(恢复后执行到 return,生成器结束)

1.2 生成器对象的核心方法

生成器对象(比如上面的 generator)有三个关键方法,用来控制函数执行:

方法 作用 示例
next(value) 恢复执行,可给上一个 yield 传值 generator.next('传入的数据')
return(value) 提前结束生成器,后续 next() 都会返回 done: true generator.return('提前终止')
throw(error) 向生成器内部抛错,可在生成器里用 try/catch 捕获 generator.throw(new Error('出错了'))

1.3 必须注意的两个点

  1. 箭头函数不能做生成器
    箭头函数没有自己的 thisarguments,也不支持 yield,所以没法写成 ()* => {} 这种形式。

  2. 调用生成器不立即执行
    和普通函数不同,stepGenerator() 调用后不会跑函数体,而是先返回生成器对象 —— 必须调用 next() 才会开始执行。

二、yield:生成器的 “暂停开关” 与 “数据通道”

yield 不只是 “暂停键”,还能在生成器和外部之间传递数据,相当于一个 “双向通道”。

2.1 yield 的两个核心作用

  1. 暂停执行,向外传值
    yield 表达式 会让函数暂停,同时把 “表达式的值” 传给外部(通过 next().value 获取):
1
2
3
4
5
6
7
8
function* fruitGenerator() {
yield '苹果'; // 暂停时,向外传“苹果”
yield '香蕉'; // 再次暂停时,向外传“香蕉”
}

const gen = fruitGenerator();
console.log(gen.next().value); // 拿到“苹果”
console.log(gen.next().value); // 拿到“香蕉”
  1. 接收外部传入的值
    外部调用 next(值) 时,这个 “值” 会作为上一个 yield 表达式的返回值,传给生成器内部:
1
2
3
4
5
6
7
8
9
10
11
12
function* chatGenerator() {
// 第一个 yield:向外传“你叫什么名字?”,暂停后等待外部传值
const name = yield '你叫什么名字?';
// 外部传值后,恢复执行:用收到的 name 拼接新内容,再向外传
yield `你好,${name}!`;
}

const chat = chatGenerator();
// 第一次 next():执行到第一个 yield,拿到“你叫什么名字?”
console.log(chat.next().value);
// 第二次 next('小明'):把“小明”传给上一个 yield,作为 name 的值,执行到第二个 yield
console.log(chat.next('小明').value); // 输出“你好,小明!”

注意: next(值) 的 “值”只给上一个 yield。如果是第一次调用 next(),传值是无效的 —— 因为此时还没有执行过任何 yield

2.2 yield 的使用限制

yield 只能在生成器函数内部用,哪怕是生成器里的嵌套函数也不行,否则会报错:

1
2
3
4
5
6
function* wrongGenerator(items) {
items.forEach(item => {
// 错误:yield 不能在 forEach 的回调里用(超出了生成器函数的直接作用域)
yield item + 1;
});
}

原理和 return 类似:嵌套函数的 return 不能直接让外层函数返回,yield 也无法跨越函数边界。

三、生成器的实际用途:这些场景用它很顺手

生成器不是花架子,实际开发中很多场景都能用到,分享几个高频用法:

3.1 自定义迭代器:轻松遍历复杂数据

JavaScript 里的 for...of 循环需要 “可迭代对象”(比如数组、字符串),而生成器可以轻松创建自定义的迭代器,不用手动实现 Symbol.iterator

比如实现一个 “指定范围的数字迭代器”:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 生成 从 start 到 end、步长为 step 的数字
function* range(start, end, step = 1) {
for (let i = start; i <= end; i += step) {
yield i; // 每次迭代返回一个数字
}
}

// 用 for...of 遍历生成器(生成器对象默认是可迭代的)
for (const num of range(1, 5, 2)) {
console.log(num); // 输出 1、3、5
}

// 也可以用 [...gen] 转成数组
const numArray = [...range(2, 6)];
console.log(numArray); // 输出 [2, 3, 4, 5, 6]

3.2 无限序列:按需生成,不占内存

如果需要无限循环的序列(比如斐波那契数列),用普通函数会陷入死循环,但生成器可以 “按需产出”,想要多少要多少:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 生成无限的斐波那契数列
function* fibonacci() {
let [prev, curr] = [0, 1];
while (true) {
// 无限循环,但不会卡,因为每次 yield 都会暂停
yield curr; // 产出当前项
[prev, curr] = [curr, prev + curr]; // 更新下一项
}
}

const fib = fibonacci();
// 想要几项就调用几次 next()
console.log(fib.next().value); // 1
console.log(fib.next().value); // 1
console.log(fib.next().value); // 2
console.log(fib.next().value); // 3
console.log(fib.next().value); // 5
// 什么时候停,完全由外部控制

3.3 异步流程管理:async / await 之前的方案

在 async / await(👉 Async / Await:用同步的方式写异步,真香!)普及前,生成器常用来简化异步代码(避免回调地狱)。核心思路是:用 yield 暂停等待异步结果,拿到结果后再恢复执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// 模拟两个异步请求
function fetchUser() {
return new Promise((resolve) => setTimeout(() => resolve({ id: 1, name: '小明' }), 1000));
}
function fetchUserOrders(userId) {
return new Promise((resolve) => setTimeout(() => resolve([{ id: 101, goods: '手机' }]), 1000));
}

// 用生成器管理异步流程
function* asyncFlow() {
// 等待 fetchUser 完成,拿到用户数据
const user = yield fetchUser();
// 用用户 id 发起第二个请求,等待完成
const orders = yield fetchUserOrders(user.id);
// 返回最终结果
return { user, orders };
}

// 自动执行器:帮我们调用 next(),不用手动处理异步
function runAsync(gen) {
const generator = gen();

// 递归处理异步结果
function handleResult(result) {
if (result.done) {
// 生成器结束,返回最终结果
return Promise.resolve(result.value);
}
// 结果是 Promise,等待完成后把数据传给下一个 next()
return result.value.then((data) => handleResult(generator.next(data)));
}

return handleResult(generator.next());
}

// 执行异步流程
runAsync(asyncFlow).then((result) => {
console.log('最终结果:', result);
// 2秒后输出:{ user: { id:1, name:'小明' }, orders: [{ id:101, goods:'手机' }] }
});

现在虽然 async/await 更常用,但理解这种思路,能帮你更好地掌握异步编程的本质。

3.4 状态机:清晰管理状态切换

比如交通灯的 “红 → 绿 → 黄” 循环,用生成器实现状态切换,逻辑特别清晰:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 交通灯状态机
function* trafficLight() {
while (true) {
yield '红灯:等待3秒';
yield '绿灯:通行5秒';
yield '黄灯:准备2秒';
}
}

const light = trafficLight();
// 每次调用 next(),切换一次状态
console.log(light.next().value); // 红灯:等待3秒
console.log(light.next().value); // 绿灯:通行5秒
console.log(light.next().value); // 黄灯:准备2秒
console.log(light.next().value); // 红灯:等待3秒(循环)

四、yield*:生成器的 “委托执行”

如果一个生成器需要调用另一个生成器,可以用 yield* 实现 “委托”—— 让被调用的生成器先执行完,再回到当前生成器。

4.1 基本用法:委托执行其他生成器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 生成器 A
function* generatorA() {
yield 'A1';
yield 'A2';
}

// 生成器 B:委托 generatorA 执行
function* generatorB() {
yield 'B1';
yield* generatorA(); // 委托:先把 generatorA 执行完
yield 'B2';
}

// 遍历 generatorB,看看顺序
const result = [...generatorB()];
console.log(result); // 输出 ["B1", "A1", "A2", "B2"]

可以看到,yield* generatorA() 会让 generatorA 的所有 yield 先执行,再继续 generatorB 后续的代码。

4.2 实用场景:递归遍历树形结构

比如遍历 DOM 树、文件夹目录这种层级结构,用 yield* 递归委托特别方便:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 遍历树形结构(比如 DOM 树)
function* traverseTree(node) {
yield node; // 先产出当前节点

// 如果有子节点,递归委托遍历子节点
if (node.children) {
for (const child of node.children) {
yield* traverseTree(child);
}
}
}

// 遍历 body 下的所有 DOM 节点
const bodyTree = document.body;
for (const node of traverseTree(bodyTree)) {
console.log(node.tagName); // 依次输出 body、div、p、span 等标签名
}

五、生成器最佳实践:避坑与优化

5.1 资源清理:用 try…finally 确保释放

如果生成器里用到了需要手动释放的资源(比如文件句柄、网络连接),一定要用 try...finally—— 哪怕生成器被提前终止(比如调用 return()),finally 里的代码也会执行,避免资源泄漏。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function* resourceGenerator() {
// 模拟获取资源(比如打开文件)
const resource = openResource();

try {
yield '使用资源处理数据';
yield '继续使用资源';
} finally {
// 无论生成器正常结束还是提前终止,都会执行这里
closeResource(resource); // 释放资源
}
}

const gen = resourceGenerator();
gen.next(); // 执行到第一个 yield
gen.return('提前结束'); // 提前终止,会触发 finally 里的 closeResource

5.2 避免无限循环:加安全边界

除非确实需要 “无限序列”(比如斐波那契),否则一定要给生成器加终止条件 —— 避免不小心写成死循环,导致内存溢出。

1
2
3
4
5
6
7
8
// 安全的生成器:最多生成 100 个值
function* limitedGenerator() {
let count = 0;
while (count < 100) {
// 终止条件:count 到 100 就停
yield count++;
}
}

5.3 异常处理:用 throw () 抛错,try / catch 捕获

如果生成器执行中需要处理错误,可以在外部调用 generator.throw(错误),然后在生成器内部用 try/catch 捕获:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function* errorHandleGenerator() {
try {
yield '正常执行中';
yield '这一步可能出错';
} catch (err) {
// 捕获外部抛入的错误
yield `捕获到错误:${err.message}`;
}
}

const gen = errorHandleGenerator();
console.log(gen.next().value); // 输出“正常执行中”
// 向生成器抛错
console.log(gen.throw(new Error('测试错误')).value); // 输出“捕获到错误:测试错误”

六、生成器 vs 普通函数:核心差异对比

特性 普通函数 生成器函数
执行流程 一旦开始,必须执行完 可暂停(yield)、可恢复(next()
返回值 只能返回一次值(return 可多次返回值(yield),最后一次 return 是收尾
内存效率 一次性处理所有数据,大数据易内存溢出 按需生成值,处理大数据更高效
状态保持 执行完后局部变量销毁,不保持状态 暂停时保留局部变量状态,恢复后继续使用
定义语法 function 函数名() {} function* 函数名() {}

生成器的核心价值在于 “可控的执行流程” 和 “按需产出数据”—— 它不像 async/await 那样专门解决异步问题,而是在迭代、状态管理、大数据处理等场景都能发挥作用。如果你需要更灵活地控制函数执行,或者想简化复杂的遍历逻辑,不妨试试生成器~