你以为数组链式操作每次都要生成中间数组?今天 ES2025 Iterator Helpers 用延迟求值把这件事彻底原生化了
你以为数组链式操作每次都要生成中间数组?今天 ES2025 Iterator Helpers 用延迟求值把这件事彻底原生化了
做列表筛选的时候,你大概写过这样的代码:
const visible = users
.filter(u => u.active)
.map(u => u.name)
.slice(0, 10);
看起来干净,对吧?代价藏在幕后——filter 生成一个包含所有活跃用户的新数组,map 再对这个新数组操作生成另一个新数组,即使你只想要 10 个字符串,中间的每一步都在凭空创造本不需要的数组。
问题不是代码写错了,而是数组方法天生 eager(立即求值)。 每一次 .filter()、.map() 调用都立刻在内存里物化一个完整数组,然后才交给下一个方法继续处理。数据量小的时候感知不到;一旦列表里有几万条记录、或者从网络流里读取,浪费就开始了。
延迟求值:只取需要的那部分
ES2025 原生支持了 Iterator Helpers(TC39 Stage 4,2024 年 10 月通过),给你一套挂在 Iterator.prototype 上的链式方法——而迭代器天然支持 lazy 惰性求值。
用 Iterator Helpers 改写上面的逻辑:
const visible = users.values()
.filter(u => u.active)
.map(u => u.name)
.take(10)
.toArray();
.values() 把数组转成迭代器;.filter()、.map()、.take() 都是 lazy 的——它们只返回一个包装迭代器,什么都不做;直到最后调用 .toArray() 才真正开始执行,而且取满 10 个就立刻停手,中间的数据永远不会被完整遍历。
真正跑起来差多少
dev.to 上有开发者做了实测:50,000 条用户数据,取前 10 个活跃用户名。
用 eager 数组链:
filter遍历全部 50,000 条 → 分配 ~32,000 活跃用户的数组map遍历那 32,000 条 → 分配 32,000 个名字字符串slice取前 10 个 → 最终只有 10 个存活
大约 82,000 次元素访问,4 次中间数组分配。
用 Iterator Helpers lazy 链:
- 取前 10 个活跃用户就停止,假设前 200 条里就有 10 个活跃
- 只需 ~200 次元素访问,1 次最终数组分配
性能差距在数据规模越大、越早找到目标时越明显——处理百万级日志流、实时数据管道、或者 UI 只显示前 N 条的场景下,Iterator Helpers 可以把内存占用从 O(n) 降到 O(k),k 是你实际需要的条目数。
完整 API 一览
Iterator Helpers 分两类:
Lazy 方法(返回包装迭代器,不立即执行):
.map(fn)—— 变换.filter(fn)—— 筛选.take(n)—— 取前 n 个(重要:让无限迭代器可终止).drop(n)—— 跳过前 n 个.flatMap(fn)—— 拍平
Eager 方法(消费迭代器,返回最终值):
.toArray()—— 物化数组.reduce(fn, init)—— 聚合.find(fn)—— 找第一个匹配(短路,遇到即停).some(fn)/.every(fn)—— 布尔判断.forEach(fn)—— 遍历
无限迭代器终于可用了
这是数组方法做不到的事——无限生成器无法用 .filter() 处理,因为 filter 需要完整数组才会开始输出。Iterator Helpers 是 lazy 的,take(n) 会在恰好产生 n 个结果后停止取数,让无限生成器真正可用:
function* fibonacci() {
let [a, b] = [0, 1];
while (true) {
yield a;
[a, b] = [b, a + b];
}
}
// 取前 10 个偶数
const evenSquares = Iterator.from(fibonacci())
.filter(n => n % 2 === 0)
.map(n => n * n)
.take(10)
.toArray();
不需要预先知道上界,不需要手动管理游标——take(n) 接管了控制流。
兼容性:不用再等了
截至 2025 年 3 月(Baseline Newly Available),所有主流浏览器均已支持:
- Chrome 122+
- Firefox 131+
- Safari 18.2+
- Node.js 22 LTS / 24
- Deno 2
- Bun 1.1.31+
TypeScript 5.6 也同步更新了 lib.es2025.iterator.d.ts 类型定义。
什么时候不该换
- 数据量很小(几十条), eagerness 的开销可以忽略不计,重构收益不大
- 需要在同一个数据集上做两次遍历(迭代器是一次性的,用
.toArray()物化后再操作) - 已经在用 lodash/fp/ramda 的 lazy 版本且运行正常——迁移有成本
但如果你的场景是大数据、实时流、或者 UI 只取前几条,Iterator Helpers 直接原生替代,不需要任何依赖。
这件事的本质变化不是多了一个 API,而是 JavaScript 终于有了一条不走中间数组的链式操作路径。下次写 .filter().map().slice(0, n) 之前,问自己一句:我真的需要那 n 个,还是在帮 CPU 和内存找事做?
评论区
登录后可评论。