迭代器与生成器
for...of、展开运算符、解构这些日常语法背后,都依赖同一套机制:可迭代协议(Iterable) 与 迭代器协议(Iterator)。而生成器(Generator)则提供了一种编写迭代器的声明式方式,还能实现暂停与恢复、惰性求值等高级能力。本文从协议讲起,逐步深入到生成器与异步迭代。
一、两个协议
1.1 可迭代协议(Iterable)
一个对象实现了 Symbol.iterator 方法,就称为可迭代对象。该方法返回一个迭代器:
const iterable = {
[Symbol.iterator]() {
return iterator; // 返回迭代器
},
};1.2 迭代器协议(Iterator)
一个对象实现了 next() 方法,就称为迭代器。next() 返回形如 { value, done } 的对象:
value:当前元素的值;done:布尔值,false表示还有下一个,true表示迭代结束(此时value通常省略)。
// 一个最简单的迭代器:依次返回 1、2、3
const iterator = {
index: 0,
next() {
if (this.index < 3) {
return { value: ++this.index, done: false };
}
return { value: undefined, done: true };
},
};
console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next()); // { value: 2, done: false }
console.log(iterator.next()); // { value: 3, done: false }
console.log(iterator.next()); // { value: undefined, done: true }| 协议 | 判定依据 | 作用 |
|---|---|---|
| 可迭代协议 | 存在 Symbol.iterator 方法 | 可以被 for...of 等消费 |
| 迭代器协议 | 存在 next() 方法 | 逐个产出值,通过 done 表示结束 |
二、内置可迭代对象
以下内置对象原生实现了可迭代协议:
// Array
for (const v of [10, 20]) console.log(v); // 10 20
// String(按字符迭代)
for (const c of "你好") console.log(c); // 你 好
// Map(迭代键值对)
for (const [k, v] of new Map([["a", 1]])) console.log(k, v); // a 1
// Set
for (const v of new Set([1, 2])) console.log(v); // 1 2
// arguments(类数组也可迭代)
function sum() {
let total = 0;
for (const n of arguments) total += n;
return total;
}
console.log(sum(1, 2, 3)); // 6
// 其他:TypedArray、NodeList、DOM 集合等普通对象({})不可迭代,直接 for...of 会报错 TypeError: obj is not iterable。
三、for...of 的内部机制
for...of 的执行过程相当于:
// for (const v of iterable) { ... } 等价于:
const it = iterable[Symbol.iterator]();
let result = it.next();
while (!result.done) {
const v = result.value;
// 循环体
result = it.next();
}验证 Symbol.iterator 的存在性:
const arr = [1, 2, 3];
console.log(typeof arr[Symbol.iterator]); // function
// 手动调用迭代器
const it = arr[Symbol.iterator]();
console.log(it.next()); // { value: 1, done: false }
console.log(it.next()); // { value: 2, done: false }
console.log(it.next()); // { value: 3, done: false }
console.log(it.next()); // { value: undefined, done: true }四、自定义迭代器
给任意对象实现 Symbol.iterator,即可让它支持 for...of、展开、解构:
const range = {
from: 1,
to: 5,
[Symbol.iterator]() {
let current = this.from;
const end = this.to;
return {
next() {
if (current <= end) {
return { value: current++, done: false };
}
return { value: undefined, done: true };
},
};
},
};
for (const n of range) console.log(n); // 1 2 3 4 5
console.log([...range]); // [1, 2, 3, 4, 5]4.1 迭代器自身也可迭代
让迭代器返回自身,可以做到"迭代器 = 可迭代对象":
const iterableIterator = {
index: 0,
[Symbol.iterator]() {
return this; // 返回自身
},
next() {
return this.index < 3
? { value: this.index++, done: false }
: { value: undefined, done: true };
},
};
for (const v of iterableIterator) console.log(v); // 0 1 2五、生成器函数
5.1 function* 与 yield
用 function* 声明的函数是生成器函数,调用它不会立即执行函数体,而是返回一个生成器对象(既是可迭代对象也是迭代器)。yield 关键字可以暂停函数执行并向外产出值:
function* gen() {
yield 1;
yield 2;
yield 3;
}
const g = gen();
console.log(g.next()); // { value: 1, done: false }
console.log(g.next()); // { value: 2, done: false }
console.log(g.next()); // { value: 3, done: false }
console.log(g.next()); // { value: undefined, done: true }
// 生成器对象可直接 for...of
for (const v of gen()) console.log(v); // 1 2 35.2 暂停与恢复
每次调用 next(),函数从上次 yield 的位置继续执行,直到遇到下一个 yield 或函数结束:
function* countdown() {
console.log("开始");
yield 3;
console.log("2...");
yield 2;
console.log("1...");
yield 1;
console.log("发射");
}
const it = countdown();
it.next(); // 打印:开始
it.next(); // 打印:2...
it.next(); // 打印:1...
it.next(); // 打印:发射5.3 next() 传参与 yield 的返回值
yield 表达式本身也有值——即下一次 next(value) 传入的值。这让调用方可以与生成器双向通信:
function* dialogue() {
const name = yield "你叫什么名字?";
const age = yield "你多大了?";
return `我叫${name},今年${age}岁`;
}
const it = dialogue();
console.log(it.next()); // { value: '你叫什么名字?', done: false }
console.log(it.next("张三")); // { value: '你多大了?', done: false }
console.log(it.next(25)); // { value: '我叫张三,今年25岁', done: true }5.4 惰性求值
生成器是**惰性(lazy)**的:只有调用 next() 时才执行对应代码,不会一次性把所有值算出来。这对无限序列尤其重要:
function* infinite() {
let i = 0;
while (true) {
yield i++;
}
}
const it = infinite();
console.log(it.next().value); // 0
console.log(it.next().value); // 1
console.log(it.next().value); // 2
// 只取前三个,不会因为 while(true) 而崩溃六、yield* 委托
yield* 可以把迭代"委托"给另一个可迭代对象或生成器,等价于逐个产出其中的值:
function* inner() {
yield 2;
yield 3;
}
function* outer() {
yield 1;
yield* inner(); // 委托给 inner
yield 4;
}
console.log([...outer()]); // [1, 2, 3, 4]
// 委托给数组
function* flattenPart() {
yield* [5, 6, 7];
}
console.log([...flattenPart()]); // [5, 6, 7]七、生成器与迭代器的关系
| 对比项 | 普通迭代器 | 生成器 |
|---|---|---|
| 编写方式 | 手写 next(),维护状态变量 | 用 yield 天然保存函数内状态 |
| 可读性 | 状态逻辑分散 | 线性书写,逻辑集中 |
| 双向通信 | 需要额外设计 | next(value) 原生支持 |
| 代码量 | 多 | 少 |
两者本质是同一套协议:生成器对象就是"自动生成的迭代器"。
// 用生成器重写上面的 range,代码简洁得多
function* range(from, to) {
for (let i = from; i <= to; i++) {
yield i;
}
}
console.log([...range(1, 5)]); // [1, 2, 3, 4, 5]八、生成器的实际应用
8.1 无限序列
function* fibonacci() {
let a = 0;
let b = 1;
while (true) {
yield a;
[a, b] = [b, a + b];
}
}
// 取出前 10 个斐波那契数
const fib = fibonacci();
console.log(Array.from({ length: 10 }, () => fib.next().value));
// [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]8.2 状态机
用生成器实现"红灯 → 绿灯 → 黄灯"循环的状态机:
function* trafficLight() {
const lights = ["红", "绿", "黄"];
let index = 0;
while (true) {
yield lights[index];
index = (index + 1) % lights.length;
}
}
const light = trafficLight();
console.log(light.next().value); // 红
console.log(light.next().value); // 绿
console.log(light.next().value); // 黄
console.log(light.next().value); // 红(循环)8.3 扁平化嵌套数组
利用 yield* 递归展平任意层级的嵌套数组:
function* flatten(arr) {
for (const item of arr) {
if (Array.isArray(item)) {
yield* flatten(item); // 递归委托
} else {
yield item;
}
}
}
const nested = [1, [2, [3, 4]], 5];
console.log([...flatten(nested)]); // [1, 2, 3, 4, 5]8.4 分批处理大数据
从"数据源"按需取数据,避免一次性载入内存:
function* paginate(pageSize) {
let page = 1;
while (true) {
// 模拟请求第 page 页数据
const data = Array.from({ length: pageSize }, (_, i) => page * pageSize + i);
yield data;
page++;
}
}
const pages = paginate(3);
console.log(pages.next().value); // [3, 4, 5]
console.log(pages.next().value); // [6, 7, 8]九、异步迭代器与 for-await-of
9.1 Symbol.asyncIterator
异步迭代器用 next() 返回 Promise,配合 for await...of 消费:
const asyncRange = {
from: 1,
to: 3,
[Symbol.asyncIterator]() {
let current = this.from;
return {
next: () =>
new Promise((resolve) => {
setTimeout(() => {
if (current <= this.to) {
resolve({ value: current++, done: false });
} else {
resolve({ value: undefined, done: true });
}
}, 100);
}),
};
},
};
(async () => {
for await (const n of asyncRange) {
console.log(n); // 每隔 100ms 输出 1、2、3
}
})();9.2 异步生成器
用 async function* 声明异步生成器,内部可以 await,对外产出 Promise:
async function* fetchPages() {
for (let page = 1; page <= 3; page++) {
const data = await fetch(`/api/data?page=${page}`);
yield data.json();
}
}
(async () => {
for await (const pageData of fetchPages()) {
console.log(pageData);
}
})();9.3 同步与异步迭代对比
| 对比项 | 同步迭代 | 异步迭代 |
|---|---|---|
| 标记 | Symbol.iterator | Symbol.asyncIterator |
| 遍历语法 | for...of | for await...of |
next() 返回值 | 普通对象 { value, done } | Promise 解析为 { value, done } |
| 生成器 | function* | async function* |
十、展开运算符与解构
10.1 展开可迭代对象
console.log(...new Set([1, 2, 3])); // 1 2 3
console.log([...new Map([["a", 1]])]); // [['a', 1]]
console.log([..."abc"]); // ['a', 'b', 'c']10.2 解构可迭代对象
const [x, y] = new Set([1, 2, 3]);
console.log(x, y); // 1 2
// Map 迭代出键值对,也可用解构取前几个条目
const m = new Map([["name", "张三"], ["age", 25], ["city", "北京"]]);
const [[k1, v1], [k2, v2]] = m;
console.log(k1, v1, k2, v2); // name 张三 age 25
// 对象键值对写法:Map 转对象后再解构
const { name, age } = Object.fromEntries(m);
console.log(name, age); // 张三 2510.3 字符串迭代与字符展开
字符串按 Unicode 码点迭代,展开运算符可用于正确拆分代理对字符(如 emoji):
console.log("😀".length); // 2(代理对占 2 个码元)
console.log([..."😀"].length); // 1(按码点迭代)