C01 迭代器与生成器
C01 迭代器与生成器
摘要:可迭代协议要求对象实现
Symbol.iterator,返回带next()的迭代器;next()每次返回{value, done}。生成器函数用yield把执行暂停,是编写迭代器最短的方式,也是惰性序列的实现手段。for...of、展开、解构、Array.from都依赖该协议。本文给出自定义可迭代对象、生成器分页与无限序列背压三组实现。
关键词:迭代器,可迭代协议,生成器,惰性计算,yield
所属:frontend/js / C-高级 | 上一篇:B06-错误处理与调试 | 下一篇:C02-属性描述符与Proxy
1 引言
需要对一个分页接口逐条处理全部记录时,常见写法是把所有页拼成一个大数组再遍历,内存占用与数据量成正比。惰性序列可以做到取一条算一条。
本文回答三个问题:迭代协议包含什么、生成器如何暂停执行、惰性序列在什么条件下值得用。范围见表 1-1。
| 项 | 本文覆盖 | 本文不覆盖 |
|---|---|---|
| 协议 | Symbol.iterator、next()、return()、异步迭代 |
流式 IO(C04) |
| 生成器 | yield、双向传值、return、throw |
协程式调度框架 |
| 应用 | 自定义集合、分页加载、无限序列 | 流式 IO(C04) |
2 预备知识
| 项 | 要求 | 核对方式 |
|---|---|---|
| 环境 | Node.js 20+ 或 Chrome 130+ | 支持生成器与展开语法 |
| 前置 | B05 | 能在模块中组织代码 |
| 文件 | js-practice/iterators.js |
编码 UTF-8 |
3 原理与实现
3.1 协议约定
for...of 只做三件事:取 Symbol.iterator、反复调用 next()、遇到 done 停止,如图 3-1 所示。
sequenceDiagram
participant L as for...of
participant O as 可迭代对象
participant I as 迭代器
L->>O: 读取 Symbol.iterator
O-->>L: 返回迭代器
loop 直到 done 为 true
L->>I: next()
I-->>L: { value, done }
end
L->>I: 提前退出时调用 return()
图 3-1 for…of 与迭代器之间的三次交互
图上可见:提前
break会调用迭代器的return(),这是生成器finally得以执行的原因,也是释放资源的挂载点。
3.2 完整实现
// js-practice/iterators.js;运行环境:Node.js 20+
// 1 自定义可迭代范围对象
class Range {
constructor(start, end) {
this.start = start;
this.end = end;
}
*[Symbol.iterator]() {
for (let i = this.start; i <= this.end; i += 1) {
yield i;
}
}
}
const range = new Range(1, 4);
console.log([...range]); // [1, 2, 3, 4]
const [first, ...others] = range;
console.log(first, others.length); // 1 3
// 2 生成器分页:按需取下一页
function* paginate(fetchPage, limit = 2) {
let page = 1;
while (true) {
const rows = fetchPage(page);
if (rows.length === 0) return;
yield* rows; // 委托给数组的迭代器
if (rows.length < limit) return;
page += 1;
}
}
const fakeApi = (page) => (page <= 2 ? [page * 10, page * 10 + 1] : []);
const all = [...paginate(fakeApi)];
console.log(all); // [10, 11, 20, 21]
// 3 无限序列 + 提前终止
function* naturals() {
let n = 1;
while (true) yield n++;
}
function take(iterable, count) {
const out = [];
for (const value of iterable) {
out.push(value);
if (out.length === count) break; // 触发 return()
}
return out;
}
console.log(take(naturals(), 5)); // [1, 2, 3, 4, 5]
// 4 生成器接收外部值(双向)
function* dialog() {
const name = yield 'name?';
const age = yield `hi ${name}`;
return { name, age };
}
const it = dialog();
console.log(it.next().value); // name?
console.log(it.next('Lin').value); // hi Lin
console.log(it.next(30).value); // { name:'Lin', age:30 }
// 5 提前退出会执行 finally
function* withCleanup() {
try {
yield 1;
yield 2;
} finally {
console.log('cleanup');
}
}
for (const v of withCleanup()) {
if (v === 1) break; // 输出 cleanup
}
协议成员见表 3-1。
| 成员 | 作用 | 备注 |
|---|---|---|
Symbol.iterator |
返回迭代器 | 对象实现即可用于 for...of |
next(value) |
推进一次,返回 {value, done} |
参数会作为 yield 的返回值 |
return(value) |
提前结束,触发 finally |
break 与异常时自动调用 |
throw(error) |
向生成器内部抛错 | 可在 try/catch 中处理 |
适用与不适用见表 3-2。
| 场景 | 是否适用惰性序列 | 原因 |
|---|---|---|
| 分页接口逐条处理 | 适用 | 内存与当前页同阶 |
| 无限或超大序列 | 适用 | 只计算被取用的部分 |
| 需要随机访问 | 不适用 | 迭代器只能顺序推进 |
| 需要多次遍历 | 谨慎 | 生成器一次性,需重新创建 |
3.4 异步迭代
生成器的 next() 返回 Promise 时即为异步迭代器,用 for await 逐条消费,适合分页接口与流式数据。
// js-practice/iterators.js(续);运行环境:Node.js 20+
// 1 异步生成器:await 与 yield 可以在同一个函数中使用
async function* loadPages(fetchPage) {
let page = 1;
for (;;) {
const rows = await fetchPage(page);
if (rows.length === 0) return;
yield* rows;
page += 1;
}
}
const fakeApi = async (page) => (page <= 2 ? [page * 10, page * 10 + 1] : []);
for await (const row of loadPages(fakeApi)) {
console.log(row); // 10 11 20 21,各占一行
}
// 2 手动驱动:next() 返回 Promise
const it = loadPages(fakeApi);
console.log((await it.next()).value); // 10
console.log((await it.next()).value); // 11
// 3 提前退出同样触发 finally
async function* withCleanupAsync() {
try {
yield 1;
yield 2;
} finally {
console.log('async cleanup');
}
}
for await (const v of withCleanupAsync()) {
if (v === 1) break; // 输出 async cleanup
}
两种迭代协议对照见表 3-3。
| 项 | 同步迭代 | 异步迭代 |
|---|---|---|
| 协议入口 | Symbol.iterator |
Symbol.asyncIterator |
next() |
返回 {value, done} |
返回 Promise<{value, done}> |
| 消费语法 | for...of、展开 |
for await...of |
| 生成器 | function* |
async function* |
| 数据来源 | 内存集合、计算序列 | 分页请求、流、文件读取 |
表 3-3 同步与异步迭代协议的差异
4 验证与讨论
- 执行脚本,确认输出
[1, 2, 3, 4]、[10, 11, 20, 21]、[1, 2, 3, 4, 5]。 - 把
take(naturals(), 5)改为[...naturals()],确认进程卡死,理解无限序列必须有终止条件。 - 删除
withCleanup的finally,再次执行break,确认不再输出cleanup。 - 用
Array.from(range)与[...range]各取一次,确认结果一致。 - 执行 3.4 节代码,确认
for await依次输出10、11、20、21,并在break后输出async cleanup。
| 检查项 | 预期结果 | 说明 |
|---|---|---|
| 协议生效 | 展开得到数组 | 依赖 Symbol.iterator |
| 惰性分页 | 只请求到空页为止 | 生成器驱动 |
| 提前终止 | 输出 cleanup |
break 触发 return() |
| 双向传值 | it.next('Lin') 生效 |
参数成为上一个 yield 的值 |
| 异步迭代 | for await 逐条消费 |
表 3-3 |
讨论与局限。 生成器对象是一次性的,再次遍历需重新调用生成器函数;把生成器缓存成变量后重复 for...of 只会得到空循环。另,惰性并不减少单次请求的耗时,只降低峰值内存。
5 常见问题
| 现象 | 原因 | 处理 |
|---|---|---|
obj is not iterable |
未实现 Symbol.iterator |
加 *[Symbol.iterator]() |
第二次 for...of 没有元素 |
生成器已耗尽 | 每次遍历重新调用生成器函数 |
finally 未执行 |
用 next() 手动推进后放弃 |
显式调用 iterator.return() |
| 无限序列卡死 | 缺少终止条件 | 用 take 或加 break |
yield 写在普通函数里报错 |
缺少 * |
改为 function* |
6 结论
| 序号 | 结论 | 依据 |
|---|---|---|
| 1 | 实现 Symbol.iterator 即可用于全部迭代语法 |
图 3-1 |
| 2 | 生成器是编写迭代器最短的方式 | 3.2 节 |
| 3 | 提前退出会调用 return() 并执行 finally |
图 3-1 |
| 4 | 惰性序列降低峰值内存,不减少总耗时 | 表 3-2 |
- 上一篇:C00-总结
- 下一篇:C02-属性描述符与Proxy
- 返回:C00-总结 | 科目 README
参考文献
[1] MDN Web Docs · 迭代器和生成器,https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Iterators_and_generators(访问于 2026-10-31)
[2] ECMA-262 · 27.5 Generator Objects,https://tc39.es/ecma262/#sec-generator-objects(访问于 2026-10-31)

评论 0
还没有评论,快来抢沙发吧~