生成器与迭代器
能暂停、能恢复的函数,数据流的控制艺术
🎯 学习目标
学完本章,你将能够:
- 能够解释迭代协议(Iterable 和 Iterator),并创建自定义可迭代对象
- 能够使用 function* 和 yield 定义生成器函数,实现惰性求值
- 能够使用 yield* 委托调用其他生成器,组合多个数据流
- 能够使用生成器实现无限序列和树形结构遍历
- 能够使用异步生成器(async function*)处理流式数据
一个普通函数做不到的事
怎么让函数"说到一半停下来",稍后再继续?
// 普通函数:一跑到底,中途停不下来
function normalLoop() {
let result = [];
for (let i = 0; i < 1000000; i++) {
result.push(i * 2);
}
return result; // 一次性返回 100 万个元素
}
// 生成器:按需产出,要一个给一个
function* lazyLoop() {
let i = 0;
while (true) {
yield i++ * 2; // 每次只产出一个
}
}
let gen = lazyLoop();
console.log(gen.next().value); // 0
console.log(gen.next().value); // 2
console.log(gen.next().value); // 4
普通函数要么一口气跑完,要么不跑。但有时候你需要的是"按需产出"——用到多少算多少,不浪费内存。
这就是生成器(Generator)的能力:能暂停、能恢复、能按需产出数据。
Part 1: 迭代器
生成器是迭代器的一种实现,先理解迭代器协议,后面就水到渠成
刚才说生成器能"按需产出",但这套机制底层靠的是什么?答案是一个叫迭代器协议的东西。只要一个对象有 next() 方法、返回 { value, done },它就是迭代器——不管是数组、字符串,还是你自己手写的对象。搞懂这套协议,后面学生成器就像拆礼物一样轻松。
迭代协议
只要实现 next(),就是迭代器
可迭代协议:一个对象如果实现了 [Symbol.iterator]() 方法,它就是"可迭代的"。数组、字符串、Map、Set 都内置了这个方法。
迭代器协议:一个对象如果有 next() 方法,且返回 { value, done } 格式,它就是迭代器。
// 手动实现一个可迭代对象
let range = {
from: 1,
to: 5,
[Symbol.iterator]() {
let current = this.from;
let last = this.to;
return {
next() {
return current <= last
? { value: current++, done: false }
: { done: true };
}
};
}
};
for (let num of range) console.log(num); // 1, 2, 3, 4, 5
console.log([...range]); // [1, 2, 3, 4, 5]
Part 2: 生成器
手写迭代器太麻烦,生成器是 JS 提供的语法糖
上一节手写的那个 range 迭代器,光是 [Symbol.iterator] 和 next() 就写了一堆样板代码。有没有更简单的方式?有,就是 function* 生成器。只需在函数里写 yield,JS 引擎自动帮你实现迭代器协议。接下来看看生成器怎么声明、怎么暂停恢复、怎么产出无限序列。
function* 与 yield
星号声明,yield 交出值
生成器函数用 function* 声明。函数体内用 yield 暂停执行并产出一个值。每次调用 .next() 恢复执行到下一个 yield。
function* greet() {
yield "Hello";
yield "World";
return "Done";
}
let g = greet();
console.log(g.next()); // { value: "Hello", done: false }
console.log(g.next()); // { value: "World", done: false }
console.log(g.next()); // { value: "Done", done: true }
生成器天然是可迭代的,可以直接用于 for...of 和展开运算符:
function* colors() {
yield "red"; yield "green"; yield "blue";
}
for (let c of colors()) console.log(c); // red, green, blue
暂停与恢复
yield 是一个"断点"

图:生成器 yield 暂停与恢复执行流程
【AI 生图提示词】请生成一张中文技术示意图,风格简洁、配色柔和、适合教学: - 画面主体:展示一个生成器函数 greet() 从创建到执行完毕的完整生命周期,包含三次 next() 调用与两次 yield 暂停、一次 return 结束 - 各元素关系:从左到右的水平流程图,共 7 个节点依次用箭头连接——"调用 greet() 创建生成器" → "第 1 次 gen.next()" → "执行到 yield 'Hello',暂停,返回 {value:'Hello', done:false}" → "第 2 次 gen.next()" → "执行到 yield 'World',暂停,返回 {value:'World', done:false}" → "第 3 次 gen.next()" → "执行到 return 'Done',结束,返回 {value:'Done', done:true}" - 关键标注:三个 gen.next() 调用节点分别用蓝色、橙色、绿色圆形标注;两个 yield 暂停节点用浅灰色圆角矩形标注并在节点下方标注"暂停"字样;最后一个 return 节点用红色圆形标注并标注"结束";每个暂停节点旁边用小气泡标注返回值 {value, done} - 配色建议:浅蓝/浅灰背景,深色文字,关键箭头用橙色标注 - 画面比例:16:9 - 避免:过度装饰、3D 效果、杂乱的颜色
每次 yield 就像按了暂停键。.next() 就是按播放键。函数内部的状态(变量值、执行位置)在暂停期间被完整保留。
无限序列
while(true) 不再是死循环
function* idGenerator() {
let id = 1;
while (true) {
yield id++;
}
}
let gen = idGenerator();
console.log(gen.next().value); // 1
console.log(gen.next().value); // 2
console.log(gen.next().value); // 3
// 什么时候要,什么时候生成——永远不会溢出
用生活场景说:普通函数就像一口大锅,一次把饭全煮好。生成器就像自助餐,你点一道菜,厨师做一道,用多少做多少。
这个比喻只是为了帮助理解。生成器实际上是通过协程机制实现的——函数执行上下文在 yield 时被挂起(suspended),不是真的有厨师在等你点菜。
yield* 委托
把产出工作交给另一个生成器
function* inner() {
yield "a";
yield "b";
}
function* outer() {
yield 1;
yield* inner(); // 委托给 inner
yield 3;
}
console.log([...outer()]); // [1, "a", "b", 3]
yield* 的含义是:这里所有值由另一个生成器产出,我等它完成后再继续。
Part 3: 控制生成器
生成器不只是被动产出值,还能从外部接收值、被提前终止
前面的生成器都是"我给什么你拿什么"的单向输出。但如果我想往生成器里塞数据呢?或者中途叫它别跑了?甚至给它丢个错误进去?.next(value)、.return()、.throw() 就是这三把钥匙。掌握了它们,你就能从外部完全控制生成器的执行流程。
next 传值
从外部往生成器里"喂"数据
function* accumulate() {
let sum = 0;
while (true) {
let input = yield sum; // yield 暂停并产出 sum
// next(value) 传入的值赋给 input
if (input === null) break;
sum += input;
}
return sum;
}
let acc = accumulate();
acc.next(); // 启动,返回 { value: 0, done: false }
acc.next(10); // input=10, sum=10, 返回 { value: 10, done: false }
acc.next(20); // input=20, sum=30, 返回 { value: 30, done: false }
acc.next(null); // input=null, break, 返回 { value: 30, done: true }
.next() 只是启动生成器,传入的值会被忽略。从第二次 .next(value) 开始,值才会赋给 yield 左边的变量。
return 与 throw
提前结束和注入错误
// return(value) —— 提前终止生成器
function* gen() {
yield 1; yield 2; yield 3;
}
let g = gen();
g.next(); // { value: 1, done: false }
g.return("end"); // { value: "end", done: true }
g.next(); // { value: undefined, done: true }
// throw(error) —— 在 yield 处抛出错误
function* errorGen() {
try {
yield 1;
yield 2;
} catch (e) {
console.log("捕获:", e.message);
}
yield 3;
}
let eg = errorGen();
eg.next(); // { value: 1 }
eg.throw(new Error("oops")); // "捕获: oops"
// 返回 { value: 3, done: false }
Part 4: 实战应用
生成器在实际开发中有哪些用武之地?
理论学了一堆,该看看生成器能解决什么实际问题了。斐波那契数列不用预分配数组、树形结构遍历不用手动维护栈、大数据分块处理不会撑爆内存——这些场景用生成器写起来既简洁又高效。接下来逐个击破。
斐波那契
惰性计算无限数列
function* fibonacci() {
let [a, b] = [0, 1];
while (true) {
yield a;
[a, b] = [b, a + b];
}
}
let fib = fibonacci();
for (let i = 0; i < 10; i++) {
console.log(fib.next().value);
}
// 0, 1, 1, 2, 3, 5, 8, 13, 21, 34
如果用普通函数,你得先算出所有值存到数组里。生成器只算你需要的,内存占用恒定。
树遍历与分块
递归结构和大数据的利器
// 树遍历(前序)
function* traverseTree(node) {
yield node.value;
if (node.left) yield* traverseTree(node.left);
if (node.right) yield* traverseTree(node.right);
}
// 分块处理大数据
function* chunk(array, size) {
for (let i = 0; i < array.length; i += size) {
yield array.slice(i, i + size);
}
}
let data = Array.from({length: 100}, (_, i) => i);
for (let batch of chunk(data, 10)) {
console.log(batch); // 每批 10 个
}
Part 5: 异步生成器
异步 + 生成器 = 流式处理数据的终极武器
前面处理的都是同步数据。可真实项目里,数据往往来自网络请求、文件读取这些异步操作。能不能让生成器一边等数据、一边产出结果?async function* 就是为此而生——内部用 await 等数据,外部用 for await...of 消费。再加上管道式组合,流式数据处理变得异常优雅。
异步生成器
yield + await,双剑合璧
async function* fetchPages(url) {
let page = 1;
while (true) {
let res = await fetch(`${url}?page=${page}`);
let data = await res.json();
if (data.items.length === 0) return;
yield data.items;
page++;
}
}
async function getAllItems() {
let all = [];
for await (let items of fetchPages("/api/items")) {
all.push(...items);
}
return all;
}
异步生成器用 async function* 声明。内部可以用 await,外部用 for await...of 消费。
流式管道
组合异步生成器,像管道一样处理数据
// 过滤
async function* filterStream(source, predicate) {
for await (let item of source) {
if (predicate(item)) yield item;
}
}
// 映射
async function* mapStream(source, mapper) {
for await (let item of source) {
yield mapper(item);
}
}
// 使用——像 Linux 管道一样
async function* numbers() {
for (let i = 1; i <= 100; i++) {
yield i;
}
}
let pipeline = filterStream(
mapStream(numbers(), n => n * 2),
n => n > 50
);
for await (let val of pipeline) {
console.log(val); // 52, 54, 56, ..., 200
}
常见生成器错误
.next() 或用 for...of 来驱动。
问题一:忘记驱动生成器
// 错误:创建了生成器但没有消费
function* gen() { yield 1; yield 2; }
gen(); // 什么都不会发生!
// 正确:用 for...of 或手动 next()
for (let v of gen()) console.log(v);
问题二:混淆 return 和 yield 的值
function* gen() {
yield 1;
return 2; // return 的值 done=true
}
let g = gen();
g.next(); // { value: 1, done: false }
g.next(); // { value: 2, done: true }
// for...of 不会迭代 return 的值!
for (let v of gen()) console.log(v); // 只输出 1
惰性管道
用生成器组合数据处理管线
// 惰性 map
function* lazyMap(iterable, mapper) {
for (let item of iterable) {
yield mapper(item);
}
}
// 惰性 filter
function* lazyFilter(iterable, predicate) {
for (let item of iterable) {
if (predicate(item)) yield item;
}
}
// 惰性 take
function* lazyTake(iterable, n) {
let count = 0;
for (let item of iterable) {
if (count++ >= n) break;
yield item;
}
}
把多个生成器串起来,形成惰性管道——数据像水流一样逐个通过每个环节:
// 组合管道:取 1~100 中偶数,平方,取前 5 个
const source = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
11, 12, 13, 14, 15, 16, 17, 18, 19, 20];
const pipeline = lazyTake(
lazyMap(
lazyFilter(source, x => x % 2 === 0),
x => x * x
),
5
);
console.log([...pipeline]); // [4, 16, 36, 64, 100]
[...source].filter(...).map(...).slice(0,5) 会立即处理全部 20 个元素。生成器管道只处理到第 10 个元素就满足了条件(找到 5 个偶数并平方),后续元素根本不会触及。数据量越大,惰性优势越明显。
自定义可迭代
让任意类支持 for...of 遍历
class LinkedList {
constructor() {
this.head = null;
this.tail = null;
}
append(value) {
let node = { value, next: null };
if (!this.head) {
this.head = node;
} else {
this.tail.next = node;
}
this.tail = node;
return this;
}
// 实现 Symbol.iterator,让链表可迭代
[Symbol.iterator]() {
let current = this.head;
return {
next() {
if (current) {
let value = current.value;
current = current.next;
return { value, done: false };
}
return { done: true };
}
};
}
}
let list = new LinkedList();
list.append("Node.js").append("Python").append("Go");
// 可用于 for...of
for (let lang of list) {
console.log(lang); // Node.js, Python, Go
}
// 可用于展开运算符和解构赋值
console.log([...list]); // ["Node.js", "Python", "Go"]
let [first, ...rest] = list;
console.log(first); // "Node.js"
console.log(rest); // ["Python", "Go"]
[Symbol.iterator] 必须返回一个带有 next() 方法的对象。如果忘记在 next() 中返回 { done: true },for...of 会变成无限循环。
试一试
练习一
任务:写一个生成器函数 range(start, end),依次产出从 start 到 end 的整数。
// for (let n of range(3, 7)) console.log(n);
// 输出: 3, 4, 5, 6, 7
// 你的代码:
参考答案
function* range(start, end) {
for (let i = start; i <= end; i++) {
yield i;
}
}
练习二
任务:写一个生成器 take(gen, n),从另一个生成器中只取前 n 个值。
// let nums = take(fibonacci(), 5);
// console.log([...nums]); // [0, 1, 1, 2, 3]
// 你的代码:
参考答案
function* take(gen, n) {
for (let i = 0; i < n; i++) {
let result = gen.next();
if (result.done) break;
yield result.value;
}
}
练习三
任务:实现生成器函数 zip(...iterables),将多个可迭代对象"拉链式"合并。以最短的可迭代对象为准停止产出。
// let zipped = zip([1, 2, 3], ['a', 'b', 'c']);
// console.log([...zipped]);
// [[1, 'a'], [2, 'b'], [3, 'c']]
// 你的代码:
参考答案
function* zip(...iterables) {
let iterators = iterables.map(it => it[Symbol.iterator]());
while (true) {
let results = iterators.map(it => it.next());
if (results.some(r => r.done)) break;
yield results.map(r => r.value);
}
}
📝 要点总结
本章核心知识点
迭代协议
Symbol.iterator 定义可迭代对象
生成器函数
function* + yield 实现暂停与恢复
yield* 委托
将执行权交给另一个生成器
无限序列
生成器按需计算,支持无限数据流
异步生成器
async function* 处理异步流数据
下一章
生成器让我们能精确控制数据流。下一章回到面向对象,学习 class 与继承——用更优雅的方式组织代码结构。