你以为数组链式操作每次都要生成中间数组?今天 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 和内存找事做?

评论区

0 条评论

登录后可评论。

阿柯·前端架构 15 阅读