写过前端的人都踩过这个坑——数组配对每次都要手写 zip 函数,今天 Iterator.zip() 把这件事彻底原生化了

以前配对两个数组里的元素,前端工程师要么写个工具函数,要么手写一个 for 循环把下标同步往前跑。写完下次还要用,又得复制粘贴一遍。这种代码在代码库里散得到处都是,但凡换个语言写过的都清楚——这本质上就是个通用的「拉链操作」,根本不应该每个项目都自己造一遍轮子。

Chrome 153 把这件事用两个新方法彻底原生化了:Iterator.zip() 和 Iterator.zipKeyed(),TC39 Stage 4 标准,Firefox 也已经支持,算是今年最实用的新语言特性之一。

基本用法

Iterator.zip() 接受一个可迭代对象数组,返回一个迭代器,每次 yield 一个数组,数组里第 i 个元素来自第 i 个输入的可迭代对象:

const numbers = [1, 2, 3];
const letters = ["a", "b", "c"];

for (const [num, letter] of Iterator.zip([numbers, letters])) {
  console.log(`${num}-${letter}`);
}
// 输出: 1-a, 2-b, 3-c

默认行为是「最短优先」——任何一个输入迭代器跑完,整个 zip 就停下来。上面的例子里 numbers 和 letters 长度一致,没问题,但如果长度不一致呢?

const a = [1, 2, 3];
const b = [4, 5]; // 短了一截

for (const pair of Iterator.zip([a, b])) {
  console.log(pair);
}
// 只输出: [1, 4], [2, 5],第三个直接丢了

三种模式:shortest / longest / strict

Iterator.zip() 第二个参数 options 里可以指定 mode:

  • “shortest”(默认):最短的跑完就停,上面就是例子
  • “longest”:全部跑完,用 padding 填充空白位置
const a = [1, 2, 3];
const b = [4, 5]; // 少一个

for (const pair of Iterator.zip([a, b], { mode: "longest" })) {
  console.log(pair);
}
// 输出: [1, 4], [2, 5], [3, undefined]

padding 可以自定义填充值:

for (const pair of Iterator.zip([a, b], { 
  mode: "longest", 
  padding: [0] 
})) {
  console.log(pair);
}
// 输出: [1, 4], [2, 5], [3, 0]
  • “strict”:如果输入长度不一致,直接抛 TypeError:
// 长度不一致时强制报错,防止静默丢数据
Iterator.zip([a, b], { mode: "strict" });
// TypeError: zipped iterables have different lengths in strict mode

zipKeyed:用对象代替数组

对应的 zipKeyed() 返回的是对象而不是数组,适合配对有名字的数据:

const names = ["Alice", "Bob", "Carol"];
const scores = [95, 87, 92];

for (const { name, score } of Iterator.zipKeyed({ 
  name: names, 
  score: scores 
})) {
  console.log(`${name}: ${score}`);
}

这个方法还有个很实用的场景——补齐不等长数据时用对象 key 而非位置:

const temps = { Beijing: [22, 25, 28], Shanghai: [24, 27] };

for (const row of Iterator.zipKeyed(temps, { mode: "longest", padding: 0 })) {
  console.log(row);
}
// { Beijing: 22, Shanghai: 24 }
// { Beijing: 25, Shanghai: 27 }
// { Beijing: 28, Shanghai: 0 }

实战场景

处理表单数据是典型场景——前后端数据结构不一致时,需要把两个数组合成一个:

const formFields = ["username", "email", "phone"];
const formValues = await fetchUserInput();

const formData = Object.fromEntries(
  Iterator.zip(formFields, formValues)
);
// { username: "...", email: "...", phone: "..." }

或者把时间序列数据和对应值配在一起:

const timestamps = [1000, 2000, 3000, 4000];
const readings = [23.5, 24.1, 25.8]; // 传感器丢了一个数据点

for (const { time, value } of Iterator.zipKeyed(
  { time: timestamps, value: readings },
  { mode: "longest", padding: null }
)) {
  console.log(`At ${time}: ${value ?? "N/A"}`);
}

一个必须知道的坑:字符串不算可迭代对象

Iterator.zip() 不接受字符串作为直接的输入——它会把字符串当成字符的迭代器,结果可能不是你想要的。如果真的要 zip 字符串,先转成数组:

// 错误示范
Iterator.zip(["abc", "xyz"]); // 每个字符都会被单独 zip
// 输出: [["a","x"], ["b","y"], ["c","z"]]

// 如果想要的是把整个字符串当成一个元素
Iterator.zip([["abc"], ["xyz"]]); // [["abc"], ["xyz"]] ✓

现状

Chrome 153 稳定,Firefox 155 Nightly 已支持(需设置 javascript.options.experimental.regexp_buffer_boundaries 偏好),Safari 暂未支持。Stage 4 已经板上钉钉,浏览器支持只是时间问题。

如果项目里还在用 lodash 的 zip / zipWith,或者自己写的 zip 工具函数,可以考虑逐步替换了。原生实现不需要任何依赖,而且迭代器是惰性的——处理大数据量时不会一次性把内存撑爆。

下一步:打开 Chrome 153 控制台,跑一下上面的示例,看看手里的代码库里有哪些地方可以换成 Iterator.zip()。从最常用的「把两个数组合成一个对象」开始,这种场景最多,也最好替换。

评论区

0 条评论

登录后可评论。

阿柯·前端架构 12 阅读