JavaScript 迭代器的基本原理
什么是迭代器
在 JavaScript 里,迭代器是一种遍历数据结构的对象,它遵循可迭代协议,通过一个名为 next 的方法逐步返回值和完成状态。每次调用 next 都会得到一个 { value, done } 的对象,其中 done 表示遍历是否结束。通过这一机制,开发者可以以统一的方式向前访问集合中的元素。
可迭代性是实现迭代的关键,一个对象只要实现 Symbol.iterator,就变成了可迭代对象,能够被 for...of 循环等构造直接使用。迭代器的设计使得数据结构的实现与遍历解耦,便于组合与扩展。
// 自定义一个简单的同步迭代器
function makeCounter(n) {let i = 0;return {next() {if (i < n) return { value: i++, done: false };return { value: undefined, done: true };},[Symbol.iterator]() { return this; }};
}for (const v of makeCounter(3)) {console.log(v); // 0, 1, 2
}
迭代器协议的核心
next() 的返回值必须是一个对象,包含 value 和 done 两个属性。当 done 为 true 时,表示遍历结束,value 的值通常是未定义。此规范保证了不同数据结构在遍历时具有一致的行为。
为了让对象可以在 for...of 等语言结构中自动使用,需要实现 Symbol.iterator,它返回一个迭代器本身或一个可迭代的迭代器对象。
生成器的语法与工作方式
生成器函数与 yield
生成器是带有星号的函数定义,例如 function* gen() { ... },它在执行到 yield 时会暂停,并在后续通过 next() 继续执行。yield 作为暂停点,允许生成器逐步产出值,而不需要一次性计算完所有内容。
通过调用生成器函数,得到一个生成器对象,该对象本身就是一个迭代器,支持 next()。生成器使异步或惰性计算的场景更易表达和维护。
// 生成器函数示例
function* gen() {yield 1;yield 2;yield 3;
}
for (const x of gen()) {console.log(x); // 1, 2, 3
}
如何暂停与继续执行
调用 next 会触发函数体的执行,在遇到 yield 时暂停,返回的对象包含暂停点的值,以及是否结束的标记。继续调用 next,生成器从暂停位置恢复执行,直到下一个 yield、return 或结束。
注意停顿点只是同步暂停,生成器不会自动执行异步操作,除非在 yield 处放置返回 Promise 的代码并结合异步模式来处理。
// 生成器中的 yield 也可携带表达式
function* lazyNumbers() {yield 1;const y = yield 2; // 这里等待外部把值传回(通过 next(value))yield y * 2;
}const g = lazyNumbers();
console.log(g.next()); // { value: 1, done: false }
console.log(g.next()); // { value: 2, done: false }
console.log(g.next(5)); // { value: 10, done: false }
console.log(g.next()); // { value: undefined, done: true }
异步迭代的核心概念
AsyncIterator 与 Symbol.asyncIterator
为了支持异步场景,JavaScript 引入了 异步迭代器协议,即通过 Symbol.asyncIterator 定义的异步可迭代对象。该对象的 next() 返回一个 Promise,解析后返回 { value, done },允许在异步流程中逐步产生值。
与同步迭代器不同,异步迭代器的每一步可能涉及 I/O、网络请求或其他异步操作,因此返回的是 Promise。
// 简单的异步可迭代对象
const asyncIterable = {[Symbol.asyncIterator]() {let i = 0;return {next() {if (i < 3) {return Promise.resolve({ value: i++, done: false });}return Promise.resolve({ value: undefined, done: true });}};}
};(async () => {for await (const x of asyncIterable) {console.log(x); // 0, 1, 2}
})();
for await...of 的工作机制
for await...of 专门用来遍历异步可迭代对象,它会等待 next() 返回的 Promise 解决,然后再进行下一轮迭代。这个语法糖极大地简化了异步数据流的处理逻辑。
在实际应用中,for await...of 常用于从远端 API、数据库游标或流式数据源中逐步消费数据。
// 使用 for await...of 消费异步生成器
async function* fetchPages(urlProvider) {let page = 1;while (true) {const url = urlProvider(page);const res = await fetch(url);if (!res.ok) break;const data = await res.json();if (!data.items || data.items.length === 0) break;yield data.items;page++;}
}async function main() {const pageSource = (p) => `https://api.example.com/items?page=${p}`;for await (const items of fetchPages(pageSource)) {console.log('page items:', items.length);}
}
main();
实战:异步迭代的场景与实现
从 API 拉取分页数据的异步迭代器
在真实场景中,分页数据的异步拉取可以通过异步生成器实现走读,每次 yield 出当前页的数据,调用方使用 for await...of 逐页处理,避免一次性加载全部数据导致的内存压力。
设计要点包括:数据分块、错误边界、返回暂停点的清晰出口,以确保在网络中断或数据异常时仍然可以进行恢复或清晰地结束迭代。
// 示例:异步分页数据的生成器
async function* paginate(fetchPage) {let page = 1;while (true) {const res = await fetchPage(page);if (!res.ok) throw new Error('网络错误');const data = await res.json();if (!Array.isArray(data.items) || data.items.length === 0) break;yield data.items;page++;}
}
处理错误与并行读取
在异步迭代场景中,错误处理成为重要的设计点,可以通过 try/catch 捕获异步生成器内部的异常,并通过 yield 将错误信息传递给消费端。对于并行化读取,通常需要控制并发度、避免过度请求。
// 错误处理与并发控制示例
async function* safeAsyncIter(asyncIter) {try {for await (const v of asyncIter) {yield v;}} catch (err) {console.error('异步迭代错误', err);// 也可选择重新抛出或中断}
}// 使用并发限制的场景示例
async function* fetchWithLimit(urls, limit = 5) {const executing = new Set();for (const url of urls) {const p = fetch(url).then(r => r.json());executing.add(p);p.finally(() => executing.delete(p));if (executing.size >= limit) await Promise.race(executing);yield p;}
}
组合模式与性能优化
将生成器与异步迭代器结合
混合使用同步生成器、异步生成器与普通迭代器,可以在数据流中实现 lazy 计算、分阶段加载以及统一的处理管道。这种组合模式在数据处理管线、事件流和模板渲染中尤为常见。
在设计时应注意接口的一致性:尽量让不同来源的数据都能通过 for await...of、或 for...of 进行消费,提升代码的可读性与维护性。
// 将同步生成器输出接入异步处理管线的示例
async function processAll(sourceIterable) {for await (const item of sourceIterable) {// 统一处理handle(item);}
}function* syncSource() {yield* [1, 2, 3];
}
const asyncIterableFromSync = (async function* () {for (const x of syncSource()) {yield x;}
})();processAll(asyncIterableFromSync);
避免过度等待与资源泄露
正确的资源管理是异步迭代稳定运行的关键,包括在适当时机结束迭代、关闭网络连接、取消未完成的请求,以及对极端情况下的超时处理。避免未完成的 Promise 造成内存泄漏或并发滥用。
此外,优先使用原生 API 提供的异步迭代接口,尽量避免自定义复杂的状态机,以减少错误点并提高可测性。
// 使用 AbortController 设计取消能力
async function* fetchPagesCancelable(fetchPage, signal) {let page = 1;while (true) {if (signal.aborted) break;const res = await fetchPage(page, { signal });if (!res.ok) break;const data = await res.json();if (!data.items.length) break;yield data.items;page++;}
}
常见陷阱与调试技巧
yield 与 await 的混用误区
一个常见的陷阱是尝试在普通生成器内直接使用 await,这在语法上是错误的,因为普通生成器不是 async 的。在非异步生成器中使用 await 将导致语法错误,需要将生成器改为异步生成器,或在 yield 处返回 Promise 并在消费端处理。

理解这一点有助于避免在初期实现时遇到难以定位的错误。
// 错误示例(普通生成器中使用 await 会报错)
// function* bad() {
// const res = await fetch('https://example.com');
// yield res;
// }// 正确做法:使用异步生成器
async function* good() {const res = await fetch('https://example.com');yield res;
}
如何在调试中查看异步迭代状态
调试异步迭代时,可以在每一次 next/yield 的点加入日志,并结合断点、Promise 链路追踪,了解 Promise 的解析顺序、并发情况,以及 for await...of 循环的推进节奏。
使用浏览器开发者工具或 Node.js 的调试器,可以在生成器、异步迭代器的实现处设置断点,观察返回的 { value, done } 的具体时序。
// 调试示例:在异步生成器内打日志
async function* logAsyncGen() {console.log('start');yield 1;console.log('mid');yield 2;console.log('end');
}(async () => {for await (const v of logAsyncGen()) {console.log('got', v);}
})();


