JavaScript Iterator Helpers终于落地:写迭代器像写数组一样顺滑

如果你写过几年代码,一定见过这种场景:一个笨重的 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...oftoArray()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

我以前写分页经常遇到“跳过多少条”的逻辑,尤其配合生成器的时候。现在可以用 droptake 很自然的表达:

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 更直接。

几个容易踩的坑

惰性和惰性,别被自己绕进去

mapfilter 本身不会消费迭代器。如果你写了下面这段代码,实际上什么事都不会发生:

const lazyIter = naturals().map((x) => {
  console.log('处理中', x);
  return x * 2;
});
// 什么都没打印

只有当你调用 toArray() 或者 for...of 遍历的时候,回调才会执行。所以如果你在中间步骤打印日志,却什么都没打印,先确认自己是否真的消费了迭代器。

无限迭代器前必须手动加 take

这大概是所有迭代器 helper 的通病了。因为无限生成器不会自动结束,所以 reducetoArrayforEach 这些会把迭代器全部消费完的操作,放到无限流上就是死循环。我有一次为了调试,直接对自然数序列调 reduce,浏览器标签页当场卡死。加一个 take(10) 再试试。

别直接修改生成器内部的局部变量

迭代器 helper 的回调函数和数组的回调函数一样,纯粹作为投影使用。别在回调里尝试做“跳出循环”的操作,没有 break 给你。想要提前终止,使用 takefind 这类本身就能终止的方法。

怎么面向老项目写一个兼容函数

如果你的项目暂时不支持,又想偷偷享受这个 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];
    }
  };
}

这个简易实现包含了最关键的三件套:mapfiltertake。够用了。不想等浏览器支持,直接拿这个顶上。当然,如果你想让调用方拿到原生的迭代器方法,还是建议引入官方 polyfill,别自己造轮子。

结束之前再啰嗦一句

Iterator Helpers 不是让你消灭生成器,也不是让你把所有数据都转成数组。它真真正正解决的是“迭代器看起来很酷,但用起来却处处别扭”的问题。

以前我们要么被迫把生成器的数据全部塞进数组里,要么在每个循环门口自己拆零件。现在好了,生成器继续写,后面的操作交给 mapfilter 这些老朋友来处理。

写代码这么多年,比起学一大堆新框架,我更愿意看到语言本身把一些基础能力补齐。本次迭代器 API 改动不大,但是每天都要用。好的东西不需要大声吆喝,写起来顺滑就完事了。

趁着版本升级,把项目里的 while 循环改成链式写法吧,体验一下“被惯坏”的感觉。

JavaScript Iterator Helpers终于落地:写迭代器像写数组一样顺滑
收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

版权声明:
本站资源有的来自互联网收集整理,本站纯免费分享提供学习使用,如果侵犯了您的合法权益,请发送邮件1506151422@qq.com联系,将会及时下架删除。
本站资源仅供研究、学习交流之用,免费开源项目不代表完全可商用,若商业用途请先咨询开发企业能否商用,否则产生的一切后果将由下载用户自行承担。
原创板块未经允许不得转载,否则将追究法律责任。

淘吗网 javascript JavaScript Iterator Helpers终于落地:写迭代器像写数组一样顺滑 https://www.taomawang.com/web/javascript/2697.html

常见问题

相关文章

猜你喜欢
发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务