如果你写过几年代码,一定见过这种场景:一个笨重的 while 循环,手动调用 next(),然后在外面包一层数组,最后才获得想要的数据。以前没有什么好办法,因为迭代器就是迭代器,它没有数组那些好用方法。
这阵子我发现 Chrome 和 Node 都对 Iterator Helpers 提供了完整支持,语法也稳定了。顿时感觉自己的遍历代码可以洗个澡了。
先回忆一下我们以前怎么处理迭代器
假设你想生成一个自然数序列,过滤出偶数,然后取前三个。按照习惯,你八成会像下面这样写:
function* naturals() {
let i = 0;
while (true) {
yield i++;
}
}
const iterator = naturals();
const result = [];
for (let i = 0; i = 3) break;
}
}
console.log(result); // [0, 2, 4]
代码不难,但每次都手写这种“读值、判断、退出”的三连,写久了真有点烦。更别提如果业务里到处是 while,那画面真的没法看。
Iterator Helpers 是什么
简单来说,就是给所有迭代器实例挂上一些类似数组的方法。不需要把迭代器转成数组,就可以直接链式操作:map 变换、filter 过滤、take 拿前面几个、drop 跳过几个、reduce 汇总,还有 toArray 转回数组。
它最大的特点是惰性。调用 map()、filter() 并不会立即执行循环,只是返回一个新的迭代器。等到真正消费这个迭代器(比如用 for...of、toArray()、reduce())的时候才会逐条取数据。这个设计非常符合迭代器的初衷。
现在再看同一段逻辑
function* naturals() {
let i = 0;
while (true) {
yield i++;
}
}
const result = naturals()
.filter((x) => x % 2 === 0)
.take(3)
.toArray();
console.log(result); // [0, 2, 4]
说实话,第一眼看到这个 API 的时候,我愣住了。原来折腾这么久的迭代器居然也能这么写。
在浏览器和 Node 里的支持情况
我现在用 Node 22 跑这些代码,一个字,稳。Chrome 从 117 开始支持,Edge 和 Opera 也是跟着 Chromium 走。Safari 在 18.0 之后支持。如果你的旧项目还没法用,可以引入 core-js 的 polyfill,因为现在 core-js 里已经包含 Iterator Helpers 了。
如果不想装 polyfill,直接用 Array.from 过渡也行,但性能上就会损失惰性求值的优势。
几个实用方法,让迭代器变成一等公民
map 和 filter
这两个几乎和数组的一样。假设你从某个配置文件里拿到一个生成器,里面是一堆商品价格,想换算为含税价格并去掉低于 10 块的:
function* getPrices() {
yield 5;
yield 20;
yield 8;
yield 100;
yield 30;
}
const results = getPrices()
.map((price) => price * 1.13)
.filter((price) => price >= 10);
console.log([...results]); // [22.6, 113, 33.9]
注意我这里用了展开运算符。用 [...results] 也是消费迭代器的一种方式,效果和 toArray() 差不多。但如果你在中间过程只想取前 n 个,那 take 就很舒服了。
take 和 drop
我以前写分页经常遇到“跳过多少条”的逻辑,尤其配合生成器的时候。现在可以用 drop 和 take 很自然的表达:
function* messages() {
let id = 1;
while (true) {
yield { id, content: `消息${id}` };
id++;
}
}
// 跳过前10条,然后拿第11到20条
const page = messages()
.drop(10)
.take(10)
.toArray();
console.log(page[0]); // { id: 11, content: '消息11' }
console.log(page.length); // 10
这种写法特别适合处理流式数据里的偏移量。当然如果你要真分页,还是后端查询来的直接。但这里表达“忽略前 N 个”非常清晰。
reduce
reduce 可以把迭代器汇总成一个值。比如处理一个巨大的数字生成器,求和。以前你得手动 next() 然后累加,现在:
function* countUp(limit) {
for (let i = 1; i <= limit; i++) {
yield i;
}
}
const sum = countUp(100)
.filter((n) => n % 2 === 0)
.reduce((acc, val) => acc + val, 0);
console.log(sum); // 2550(偶数和)
注意 reduce 会立即消费所有剩余元素,所以如果配合无限迭代器使用,记得前面要加 take 限制,否则浏览器直接卡死。这一点和数组的 reduce 逻辑是一样的。
实战:解析一个嵌套的菜单树
这个例子是我重构一段菜单权限逻辑时写的。原先代码里有一个很深的递归函数,用来收集所有权限码。后来我用生成器加 flatMap,代码整洁了不少。
先看数据模拟:
const menuTree = [
{
title: '首页',
perms: ['home:view'],
children: [],
},
{
title: '订单中心',
perms: ['order:list'],
children: [
{
title: '订单详情',
perms: ['order:detail', 'order:export'],
children: [
{
title: '退款',
perms: ['order:refund'],
children: [],
},
],
},
],
},
{
title: '用户中心',
perms: ['user:list'],
children: [
{
title: '用户管理',
perms: ['user:add', 'user:update'],
children: [],
},
],
},
];
然后我写一个生成器,深度优先遍历所有菜单节点:
function* walkTree(nodes) {
for (const node of nodes) {
yield node;
if (node.children.length > 0) {
yield* walkTree(node.children);
}
}
}
以前我要把所有权限码收集到一个数组里,需要在外围加一个递归辅助函数。现在可以直接:
const allPerms = walkTree(menuTree)
.map((node) => node.perms)
.filter((perms) => perms.length > 0)
.flatMap((perms) => perms)
.toArray();
console.log(allPerms);
这里用了两个新东西。一个是生成器的 yield*,用于递归遍历子节点;另一个是 Iteration Helpers 的 flatMap,可以直接把每个节点的 perms 数组铺平。结果就是一个不重不漏的权限字符串数组。
边角操作:some、every和find
Iterator Helpers 还加了一些判定和查找的方法。比如判断一个无限序列里是否存在某个数,可以用 some()。但一定要记得加一个 take,不然后果很明显。
function* randomNumbers(seed = 42) {
let value = seed;
while (true) {
value = (value * 1103515245 + 12345) % 2147483648;
yield value;
}
}
const hasBig = randomNumbers()
.take(1000)
.some((num) => num > 2000000000);
console.log('取前1000个数字里有超过20亿的吗?', hasBig);
find 会返回第一个满足条件的值,没有则返回 undefined。不过要注意,find 会提前中断迭代器,而 filter().take(1) 也有类似效果,但语义上 find 更直接。
几个容易踩的坑
惰性和惰性,别被自己绕进去
map 和 filter 本身不会消费迭代器。如果你写了下面这段代码,实际上什么事都不会发生:
const lazyIter = naturals().map((x) => {
console.log('处理中', x);
return x * 2;
});
// 什么都没打印
只有当你调用 toArray() 或者 for...of 遍历的时候,回调才会执行。所以如果你在中间步骤打印日志,却什么都没打印,先确认自己是否真的消费了迭代器。
无限迭代器前必须手动加 take
这大概是所有迭代器 helper 的通病了。因为无限生成器不会自动结束,所以 reduce、toArray、forEach 这些会把迭代器全部消费完的操作,放到无限流上就是死循环。我有一次为了调试,直接对自然数序列调 reduce,浏览器标签页当场卡死。加一个 take(10) 再试试。
别直接修改生成器内部的局部变量
迭代器 helper 的回调函数和数组的回调函数一样,纯粹作为投影使用。别在回调里尝试做“跳出循环”的操作,没有 break 给你。想要提前终止,使用 take 或 find 这类本身就能终止的方法。
怎么面向老项目写一个兼容函数
如果你的项目暂时不支持,又想偷偷享受这个 API,可以自己包一层。定义一个函数,输入原本的迭代器,返回一个带 helper 方法的对象,核心就是把 Symbol.iterator 指向自己。
function iterable(iterator) {
return {
[Symbol.iterator]() {
return iterator;
},
map(callback) {
const self = this;
return iterable(function* () {
for (const value of self) {
yield callback(value);
}
}());
},
filter(predicate) {
const self = this;
return iterable(function* () {
for (const value of self) {
if (predicate(value)) {
yield value;
}
}
}());
},
take(limit) {
const self = this;
return iterable(function* () {
let taken = 0;
for (const value of self) {
if (taken >= limit) break;
yield value;
taken++;
}
}());
},
toArray() {
return [...this];
}
};
}
这个简易实现包含了最关键的三件套:map、filter、take。够用了。不想等浏览器支持,直接拿这个顶上。当然,如果你想让调用方拿到原生的迭代器方法,还是建议引入官方 polyfill,别自己造轮子。
结束之前再啰嗦一句
Iterator Helpers 不是让你消灭生成器,也不是让你把所有数据都转成数组。它真真正正解决的是“迭代器看起来很酷,但用起来却处处别扭”的问题。
以前我们要么被迫把生成器的数据全部塞进数组里,要么在每个循环门口自己拆零件。现在好了,生成器继续写,后面的操作交给 map、filter 这些老朋友来处理。
写代码这么多年,比起学一大堆新框架,我更愿意看到语言本身把一些基础能力补齐。本次迭代器 API 改动不大,但是每天都要用。好的东西不需要大声吆喝,写起来顺滑就完事了。
趁着版本升级,把项目里的 while 循环改成链式写法吧,体验一下“被惯坏”的感觉。

