C01 迭代器与生成器

2026-10-31 17:090 阅读约 15 分钟JavaScript

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. 执行脚本,确认输出 [1, 2, 3, 4]、[10, 11, 20, 21]、[1, 2, 3, 4, 5]。
  2. 把 take(naturals(), 5) 改为 [...naturals()],确认进程卡死,理解无限序列必须有终止条件。
  3. 删除 withCleanup 的 finally,再次执行 break,确认不再输出 cleanup。
  4. 用 Array.from(range) 与 [...range] 各取一次,确认结果一致。
  5. 执行 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

参考文献

[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

0/2000

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