1. 引言:为什么需要迭代器与生成器
在 JavaScript 中,我们每天都在和"可遍历的东西"打交道:数组、字符串、Map、Set、arguments,甚至 NodeList。for...of 能统一地遍历它们,背后靠的正是可迭代协议 与迭代器协议。
而当我们面对"数据量很大、无法一次性加载"或"异步数据源源不断"的场景时,普通的数组遍历就显得力不从心了。这时,生成器(Generator) 与 异步生成器(Async Generator) 提供了优雅的解决方案:它们让函数可以"暂停---恢复",从而实现惰性求值 与异步流程控制。
本文将从协议出发,逐步深入到生成器、yield、异步生成器,并给出可直接运行的示例。
2. 可迭代协议与迭代器协议
JavaScript 的遍历能力建立在两个协议之上,理解它们是掌握后续内容的基础。
两个协议的关系可以这样理解:
2.1 可迭代协议(Iterable Protocol)
一个对象只要实现了 Symbol.iterator 方法,并且该方法返回一个迭代器,它就是可迭代对象。
javascript
const arr = [1, 2, 3];
console.log(typeof arr[Symbol.iterator]); // "function"
const iter = arr[Symbol.iterator]();
console.log(iter.next()); // { value: 1, done: false }
console.log(iter.next()); // { value: 2, done: false }
console.log(iter.next()); // { value: 3, done: false }
console.log(iter.next()); // { value: undefined, done: true }
for...of、展开运算符 ...、解构赋值、Array.from 等语法,本质上都是在调用对象的 Symbol.iterator。
2.2 迭代器协议(Iterator Protocol)
迭代器是一个对象,它必须实现 next() 方法,返回形如 { value, done } 的结果:
value:当前产出的值;done:布尔值,true表示迭代结束。
javascript
function createRangeIterator(start, end) {
let current = start;
return {
next() {
if (current <= end) {
return { value: current++, done: false };
}
return { value: undefined, done: true };
},
// 让迭代器自身也可迭代,便于 for...of 使用
[Symbol.iterator]() {
return this;
},
};
}
for (const n of createRangeIterator(1, 5)) {
console.log(n); // 1 2 3 4 5
}
2.3 自定义可迭代对象
把两个协议结合起来,就能让任意对象支持 for...of:
javascript
const range = {
from: 1,
to: 5,
[Symbol.iterator]() {
let current = this.from;
const last = this.to;
return {
next() {
return current <= last
? { value: current++, done: false }
: { value: undefined, done: true };
},
};
},
};
console.log([...range]); // [1, 2, 3, 4, 5]
3. 生成器函数:更简洁的迭代器工厂
手写迭代器虽然直观,但状态管理繁琐。生成器函数 用 function* 声明,内部通过 yield 产出值,调用时返回一个迭代器,代码写起来像同步函数一样自然。
javascript
function* rangeGenerator(from, to) {
for (let i = from; i <= to; i++) {
yield i;
}
}
const gen = rangeGenerator(1, 5);
console.log(gen.next()); // { value: 1, done: false }
console.log(gen.next()); // { value: 2, done: false }
console.log([...rangeGenerator(1, 5)]); // [1, 2, 3, 4, 5]
生成器函数有几个关键特性:
- 调用生成器函数不会立即执行函数体,而是返回一个迭代器;
- 每次调用
next()才执行到下一个yield处并暂停; - 函数体执行完毕后,
done变为true。
3.1 生成器的执行流程
4. 深入 yield:双向通信与委托
yield 不只是"产出值",它还是一个双向通道 :next(value) 传入的参数会成为上一个 yield 表达式的返回值。
4.1 yield 作为表达式
javascript
function* dialog() {
const name = yield "你叫什么名字?";
const age = yield `你好,${name}!你多大了?`;
return `${name},${age} 岁,很高兴认识你。`;
}
const it = dialog();
console.log(it.next()); // { value: "你叫什么名字?", done: false }
console.log(it.next("小明")); // { value: "你好,小明!你多大了?", done: false }
console.log(it.next(18)); // { value: "小明,18 岁,很高兴认识你。", done: true }
注意:第一次 next() 的参数会被忽略,因为它之前没有 yield 在等待接收。
4.2 yield* 委托
yield* 可以把迭代控制权委托给另一个可迭代对象:
javascript
function* inner() {
yield 1;
yield 2;
}
function* outer() {
yield 0;
yield* inner();
yield 3;
}
console.log([...outer()]); // [0, 1, 2, 3]
4.3 return 与 throw
生成器迭代器还支持 return() 和 throw(),可用于提前终止或向生成器内部抛错:
javascript
function* task() {
try {
yield 1;
yield 2;
} finally {
console.log("清理资源");
}
}
const t = task();
console.log(t.next()); // { value: 1, done: false }
console.log(t.return(99)); // 触发 finally,输出"清理资源",返回 { value: 99, done: true }
5. 惰性求值:生成器的核心价值
生成器最大的优势是按需计算 。它不会一次性把所有结果算出来,而是每次 next() 才算一个,这对无限序列和大数据处理非常友好。
5.1 无限序列
javascript
function* naturals() {
let n = 0;
while (true) {
yield n++;
}
}
const nums = naturals();
console.log(nums.next().value); // 0
console.log(nums.next().value); // 1
console.log(nums.next().value); // 2
5.2 惰性管道
把多个生成器串联,可以构建惰性数据管道,只有真正取值时才计算:
javascript
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;
}
}
const pipeline = take(
filter(map(naturals(), (x) => x * x), (x) => x % 2 === 0),
5
);
console.log([...pipeline]); // [0, 4, 16, 36, 64]
上面的例子从无限自然数中取平方、筛偶数、只取前 5 个,全程没有产生多余计算。
惰性管道的取值过程如下,数据只有在被消费时才逐级流动:
6. 异步生成器:处理流式异步数据
当数据是异步到达的(如分页接口、WebSocket 消息、文件流),普通生成器就不够用了。异步生成器 用 async function* 声明,配合 for await...of 使用。
同步迭代与异步迭代的差异可以用一张图对比:
6.1 基本用法
javascript
async function* fetchPages() {
for (let page = 1; page <= 3; page++) {
const data = await fakeFetch(page);
yield data;
}
}
function fakeFetch(page) {
return new Promise((resolve) =>
setTimeout(() => resolve(`第 ${page} 页数据`), 300)
);
}
async function main() {
for await (const page of fetchPages()) {
console.log(page);
}
}
main();
// 第 1 页数据
// 第 2 页数据
// 第 3 页数据
6.2 异步迭代器协议
异步生成器返回的是异步迭代器 ,它的 next() 返回 Promise<{ value, done }>,并且通过 Symbol.asyncIterator 标识:
javascript
async function* gen() {
yield 1;
yield 2;
}
const asyncIter = gen();
console.log(typeof asyncIter[Symbol.asyncIterator]); // "function"
asyncIter.next().then((res) => console.log(res)); // { value: 1, done: false }
6.3 异步惰性管道
异步生成器同样可以组合成惰性管道,非常适合处理流式数据:
javascript
async function* asyncMap(iterable, fn) {
for await (const item of iterable) {
yield fn(item);
}
}
async function* asyncFilter(iterable, predicate) {
for await (const item of iterable) {
if (predicate(item)) yield item;
}
}
async function* source() {
for (let i = 1; i <= 10; i++) {
await new Promise((r) => setTimeout(r, 100));
yield i;
}
}
async function run() {
const result = asyncFilter(
asyncMap(source(), (x) => x * 10),
(x) => x % 20 === 0
);
for await (const v of result) {
console.log(v); // 20 40 60 80 100
}
}
run();
7. 实战:用异步生成器实现分页拉取
下面是一个更贴近真实业务的例子:分页拉取数据,直到没有更多为止,并在消费端做惰性处理。
javascript
async function* paginate(fetchPage, pageSize = 10) {
let page = 1;
while (true) {
const list = await fetchPage(page, pageSize);
if (!list || list.length === 0) return;
for (const item of list) {
yield item;
}
if (list.length < pageSize) return;
page++;
}
}
// 模拟接口
async function fetchPage(page, size) {
await new Promise((r) => setTimeout(r, 200));
if (page > 3) return [];
return Array.from({ length: size }, (_, i) => `item-${(page - 1) * size + i + 1}`);
}
async function consume() {
let count = 0;
for await (const item of paginate(fetchPage, 5)) {
console.log(item);
if (++count >= 8) break; // 消费端可随时中断,避免多余请求
}
}
consume();
这个模式的好处是:消费端按需拉取 ,一旦 break,生成器内部的循环也会随之终止,不会继续发起无意义的请求。
分页拉取与消费端中断的协作过程如下:
8. 常见陷阱与最佳实践
- 生成器是一次性的:迭代器耗尽后无法重置,需要重新调用生成器函数。
- 不要忘记
return的清理逻辑 :在try/finally中释放资源,return()或break都会触发finally。 - 避免在生成器中做副作用过重的操作:惰性求值意味着执行时机不确定,副作用要谨慎。
- 异步生成器中的错误处理 :用
try/catch包裹for await...of,或让生成器内部throw。 - 性能考量:生成器有额外的调用开销,对极小数据量不必强行使用。
javascript
async function safeConsume() {
try {
for await (const item of paginate(fetchPage, 5)) {
console.log(item);
}
} catch (err) {
console.error("拉取失败:", err);
}
}
9. 总结
| 概念 | 关键点 | 典型场景 |
|---|---|---|
| 可迭代协议 | 实现 Symbol.iterator |
自定义对象支持 for...of |
| 迭代器协议 | 实现 next() 返回 { value, done } |
手动控制遍历 |
| 生成器函数 | function* + yield |
简化迭代器编写 |
yield |
双向通信、yield* 委托 |
状态机、协程式流程 |
| 异步生成器 | async function* + for await...of |
分页、流式数据 |
迭代器与生成器把"遍历"从一次性计算变成了可控的、可暂停的、可组合的过程。掌握它们,不仅能写出更优雅的惰性求值代码,也能在异步数据流处理中游刃有余。