Iterator Helpers 迭代器助手
Iterator Helpers 给迭代器补上了 map / filter / take 这类链式方法。它的价值是「惰性」,不是「更好看的语法」。
1. 给迭代器补上链式方法
在 Iterator.prototype(以及 AsyncIterator.prototype)上新增了 map / filter / take / drop / flatMap / reduce / toArray 等方法,以及短路型的 some / every / find / forEach。关键点:它们返回的是新的迭代器,不是数组,所以不产生中间数组。截至 2026 年 10 月,使用前请确认目标运行环境是否支持(必要时用 core-js 垫片或手写生成器替代)。
2. 惰性求值省下的中间数组
数组链式方法的每一步都会生成一个完整的中间数组,迭代器助手不会——它返回的是新的迭代器,只有在真正被消费时才计算。最直观的例子是「从一万条里取前 5 条满足条件的」:数组写法要先把一万个元素 filter 完,迭代器写法在凑满 5 条后就不会再向后取数据。这个差别在处理文件行、分页流、无限序列时是量级级别的。
function* naturals() {
let i = 0
while (true) yield i++
}
const result = naturals()
.filter((n) => n % 3 === 0)
.map((n) => n * n)
.take(5)
.toArray() // [0, 9, 36, 81, 144]
// 数组写法:每一步都生成完整的中间数组,先过滤全部再取前 5
const arr = Array.from({ length: 10000 }, (_, i) => i)
const arrRes = arr.filter((n) => n % 2 === 0).slice(0, 5)
// 迭代器写法:凑满 5 个就不再向后消费
const itRes = naturals().filter((n) => n % 2 === 0).take(5).toArray()
drop(n) 跳过前 n 个、take(n) 取前 n 个,二者配合就能「切片」而不落数组。some / every / find 这类短路谓词尤其适合无限序列:命中即停,绝不遍历全部——数组的 .some 在无限序列上会直接死循环,迭代器版本因为惰性而安全。
// drop:跳过前 n 个;some/every 命中即停,不遍历全部
const it = naturals()
it.drop(3).take(2).toArray() // [3, 4] —— 跳过 0、1、2
naturals().some((n) => n > 100) // true,找到 101 即停
naturals().every((n) => n < 5) // false,遇到 5 即停
// find 返回第一个满足条件的元素本身(不是数组)
naturals().find((n) => n % 7 === 0 && n > 0) // 7
forEach 会消费整个迭代器但不返回结果,用在「遍历即目的」的场景;reduce 收口成单个值。要记住:这些方法返回的仍是迭代器,想拿到数组必须显式 toArray()——直接 console.log 一个迭代器看到的是空壳,这是调试时最容易踩的坑。
3. 无限序列与提前退出
最自然的搭档是生成器:产出一个不可穷举的序列,用 take 切前面一段。另一个常见场景是流式数据——逐行读大文件或逐页拉接口,边读边 filter,不必把全部内容装进内存。
异步场景同理:AsyncIterator.prototype 上有一套同名方法,配合 async function* 处理逐块到达的网络流或分页接口,不必把整页结果攒成数组再处理。
// 文件行流:只取前 10 行包含 error 的,不把整个文件读进内存
function* readLines(stream) {
for (const chunk of stream) yield* chunk.split('\n')
}
const errors = readLines(hugeStream)
.filter((line) => line.includes('error'))
.take(10)
.toArray()
一次性消费与运行环境
迭代器是一次性的:消费完就空了,第二次遍历什么都拿不到。需要多次使用要么重新生成迭代器,要么先 toArray()。另外调试体验比数组差——迭代器在控制台里看不到内容,必须转成数组才能观察,这也是它不适合当默认写法的现实原因。
惰性不代表一定更快,链太短时转数组反而更省:
// 数据量小、只需要一次结果:直接数组方法,语义清楚也更快
arr.filter(f).map(g).slice(0, 10)
// 数据量大、或者可能提前退出:迭代器链能省掉中间数组
arr.values().filter(f).map(g).take(10).toArray()
// 判断标准就一条:中间结果会不会很大、会不会用不完
什么时候仍该转数组 判据是数据形态不是新鲜度:数据已经在内存里、还要多次使用 → 数组方法更直观、更易调试;数据量大或不可穷举(流、无限序列)→ 迭代器助手。不要因为「迭代器更新」就无脑替换数组。
一个实用的判断顺序:先问「数据会超过内存吗」——会,用迭代器助手;不会,再问「要遍历几遍」——多遍且要调试,用数组。两者都满足才上迭代器。另外,迭代器助手和数组方法不是二选一:可以先用生成器产出序列、用迭代器助手做惰性筛选,最后 toArray() 落回数组交给下游——边界处转换、内部保持惰性,这是处理大文件解析与流式 ETL 最常见的组合。
迭代器助手的价值不只是「更好看的链式写法」,核心是惰性求值——中间过程不产生数组,这对大集合和无限序列是实质性的差别,语法只是附带的。
但也不是所有场景都该换。数据已经在内存里、后面还要用多次,数组方法更直观也更好调试。
要特别留意消费问题:take 之后再想遍历剩余元素是不行的,原迭代器已经被消费掉一部分了,不能当没发生过。(同步迭代器助手已随 ES2025 正式进入标准,主流引擎与 Node 22+ 均已支持;异步版本另属独立提案、进度更慢,用前需单独确认。)
同步部分已经定稿并进入 ES2025,细节以 MDN 的 Iterator 文档 与 TC39 proposal-iterator-helpers 的完成状态为准;异步版本仍在提案阶段,不要按同步的用法直接套。