es-toolkit 兼容版 zipWith 指南:用组合函数按索引合并多个数组
【免费下载链接】es-toolkitA modern JavaScript utility library that's 2-3 times faster and up to 97% smaller, a major upgrade to lodash.项目地址: https://gitcode.com/GitHub_Trending/es/es-toolkit
导读
本文围绕 es-toolkit 的 Lodash 兼容版本zipWith(位于es-toolkit/compat入口),讲解如何使用组合函数将多个数组按索引位置一一配对并生成新数组。你会掌握zipWith的完整参数约定、不等长数组的undefined填充规则、可选的组合函数行为,以及它底层复用unzip的实现原理;同时对比现代版zipWith(es-toolkit/array)的差异,并了解函数式(es-toolkit/fp)用法,方便在项目中按需选用。
什么是 zipWith
zipWith是 es-toolkit 兼容层提供的数组工具,它的作用是把多个数组"拉链"式地按索引对齐,把同一位置上的元素交给一个组合函数(combine function / iteratee)处理,再把组合结果收集成新数组返回。
最直观的用法是把两个数字数组逐位相加:
import { zipWith } from 'es-toolkit/compat'; const result = zipWith([1, 2], [3, 4], (a, b) => a + b); // result is [4, 6]它与原生zip的区别在于:zip只是把同位置的元素打包成元组,而zipWith允许你在打包的同时直接完成变换,省去一次map遍历。
兼容版与现代版的定位差异
在深入 API 之前需要先明确一点:es-toolkit 同时维护了两套zipWith。
- 现代版:从
es-toolkit/array导入(对应文档 docs/reference/array/zipWith.md),实现位于 src/array/zipWith.ts,针对常见场景做了性能优化,并且组合函数会额外收到当前索引作为最后一个参数。 - 兼容版:从
es-toolkit/compat导入,实现位于 src/compat/array/zipWith.ts,为了对齐 Lodash 行为做了额外的参数识别、空值容错等处理,因此官方文档提示其运行速度比现代版慢,并建议对性能敏感的场景优先使用现代版。本文以兼容版为主题,同时会在后文给出两者的行为对照。
使用方法
签名:zipWith(...arrs, iteratee)
兼容版zipWith接收任意数量的数组作为前置参数,最后一个参数是组合函数iteratee:
import { zipWith } from 'es-toolkit/compat'; // 两个数组逐位相加 const result1 = zipWith([1, 2, 3], [4, 5, 6], (a, b) => a + b); // Returns: [5, 7, 9] // 三个数组逐位相加 const result2 = zipWith([1, 2], [3, 4], [5, 6], (a, b, c) => a + b + c); // Returns: [9, 12] // 长度不同的数组:缺失位置传入 undefined const result3 = zipWith([1, 2, 3], [4, 5], (a, b) => (a || 0) + (b || 0)); // Returns: [5, 7, 3]从源码的重载签名可以确认,兼容版同时支持 1 到 5 个数组与一个组合函数的组合,并提供一个接受任意数量参数的兜底签名:
export function zipWith<T, U, V, W, X, R>( arr1: ArrayLike<T>, arr2: ArrayLike<U>, arr3: ArrayLike<V>, arr4: ArrayLike<W>, arr5: ArrayLike<X>, combine: (item1: T, item2: U, item3: V, item4: W, item5: X) => R ): R[];值得注意的是,兼容版的数组参数类型是ArrayLike<T>而非readonly T[],这意味着它接受类数组对象(如arguments、带有length与索引属性的对象),这是 Lodash 兼容行为的一部分。
参数说明
| 参数 | 类型 | 说明 |
|---|---|---|
...arrs | any[][] | 要合并的数组(也可传类数组对象、null或undefined) |
iteratee | Function | 组合函数,接收各数组同一位置上的元素,返回一个合并值 |
返回值
(any[]):将组合函数应用到每个索引位置后得到的新数组。
不等长数组的行为
如果传入的数组长度不同,结果数组的长度取最长数组的长度,较短数组缺失的位置以undefined传给组合函数。例如官方文档中的示例:
const result3 = zipWith([1, 2, 3], [4, 5], (a, b) => (a || 0) + (b || 0)); // 第一个数组第 3 位是 3,第二个数组没有第 3 位元素(undefined) // Returns: [5, 7, 3]对应测试 src/compat/array/zipWith.spec.ts 中的用例也验证了这一点:zipWith(array1, [], (a, b) => a + (b || 0))会返回[1, 2, 3],即空数组的每一位都以undefined参与组合。
源码级原理:基于 unzip 的两阶段实现
兼容版zipWith的实现非常简洁,核心只有两个阶段,完整代码见 src/compat/array/zipWith.ts:
export function zipWith<T, R>(...combine: Array<((...group: T[]) => R) | ArrayLike<T> | null | undefined>): R[] { let iteratee = combine.pop(); if (!isFunction(iteratee)) { combine.push(iteratee); iteratee = undefined; } if (!combine?.length) { return []; } const result = unzip(combine as ArrayLike<ArrayLike<T>>); if (iteratee == null) { return result as R[]; } return result.map(group => iteratee(...group)) as R[]; }阶段一:识别并弹出组合函数
实现先把最后一个参数pop()出来,用isFunction(见 src/predicate/isFunction.ts)判断它是否是函数:
- 如果是函数,它就被当作
iteratee; - 如果不是函数(包括
null、undefined、普通值),则把它放回参数列表,并把iteratee置为undefined。
这正是 Lodash 兼容的关键点:zipWith(array1, array2)不传组合函数时,行为退化为普通的zip(逐位打包成元组)。测试 src/compat/array/zipWith.spec.ts 中zipWith(array1, array2, null)、zipWith(array1, array2, undefined)与zipWith(array1, array2)结果一致,验证了该分支。
阶段二:先 unzip 再 map
去掉组合函数后,剩余参数交给兼容版unzip完成"按列转置":
const result = unzip(combine as ArrayLike<ArrayLike<T>>); if (iteratee == null) { return result as R[]; } return result.map(group => iteratee(...group)) as R[];- 若
iteratee为空,直接返回unzip的转置结果(即基础zip行为); - 否则对每一组同位置元素调用
iteratee(...group),把展开后的元素作为多个参数传入组合函数。
兼容版 unzip 的容错处理
兼容版unzip(见 src/compat/array/unzip.ts)在调用现代版unzip之前做了两件容错工作:
if (!isArrayLikeObject(array) || !array.length) { return []; } array = isArray(array) ? array : Array.from(array); array = (array as T[][]).filter(item => isArrayLikeObject(item));- 输入为空或不是类数组对象时返回空数组——这解释了测试中
zipWith(null)、zipWith(undefined)、zipWith()均返回[]的行为; - 把类数组对象转换为真正的数组,并过滤掉其中不是类数组对象的元素。
而现代版unzip(见 src/array/unzip.ts)则通过两层for循环完成转置,并以手动遍历求最大值代替Math.max(...arrs.map(...))的写法(源码注释明确说明这是出于性能考虑),结果数组的长度总是等于最长子数组的长度。
与现代版 zipWith 的差异对照
现代版zipWith(src/array/zipWith.ts)不经过unzip中转,而是直接一次性完成遍历:
export function zipWith<T, R>(arr1: readonly T[], ...rest: any[]): R[] { const arrs = [arr1, ...rest.slice(0, -1)]; const combine = rest[rest.length - 1] as (...items: any[]) => R; const maxIndex = Math.max(...arrs.map(arr => arr.length)); const result: R[] = Array(maxIndex); for (let i = 0; i < maxIndex; i++) { const elements: T[] = arrs.map(arr => arr[i]); result[i] = combine(...elements, i); } return result; }两者的核心差异可以总结为以下几点:
| 对比维度 | 兼容版(es-toolkit/compat) | 现代版(es-toolkit/array) |
|---|---|---|
| 实现路径 | 先unzip转置,再map组合(两阶段) | 单循环直接组合(一阶段) |
| 组合函数是否收到索引 | 否,只收到各数组的同位置元素 | 是,索引作为最后一个参数传入 |
| 数组参数类型 | ArrayLike<T>,接受类数组与空值 | readonly T[] |
| 不传组合函数时 | 退化为基础zip行为 | 需要显式传组合函数 |
| 性能定位 | 因兼容处理(isFunction、unzip容错、展开调用)较慢 | 官方推荐,性能更优 |
以"拼接字符串"为例,现代版用法:
import { zipWith } from 'es-toolkit/array'; zipWith(['a', 'b'], ['c', 'd'], ['e', 'f'], (a, b, c) => `${a}${b}${c}`); // Returns: ['ace', 'bdf'] // 不等长时,较短数组缺失位置以 undefined 传入 zipWith([1, 2], [10, 20, 30], (a, b) => (a ?? 0) + (b ?? 0)); // Returns: [11, 22, 30]注意两者处理缺位的方式:现代版使用??(空值合并)与||在语义上不同,兼容版文档示例用的是(a || 0),若数组中存在0、''等 falsy 值需留意取舍。
函数式用法(fp 版本)
es-toolkit 的 fp 模块把zipWith重写为可柯里化形式:先传入"被 zip 的数组 + 组合函数",返回一个接收管道数组的函数(见 src/fp/array/zipWith.ts),适合与pipe组合使用:
import { pipe, zipWith } from 'es-toolkit/fp'; pipe( [1, 2], zipWith([10, 20], (a, b) => a + b) ); // Returns: [11, 22]fp 版本同样遵循"取最长数组长度、缺失位置补undefined"的规则。对应测试 src/fp/array/zipWith.spec.ts 中,管道数组[1, 2, 3]与配置数组['a', 'b']组合时,第三个位置传入(3, undefined, 2),其中2正是索引参数,验证了 fp 版本会将索引一并传给组合函数。
常见场景与边界情况
综合官方文档、源码与测试(src/compat/array/zipWith.spec.ts),兼容版zipWith的边界行为总结如下:
- 多数组组合:支持 2~5 个及以上数组同时组合,组合函数参数个数与数组数量一致;
- 不等长数组:结果长度等于最长数组长度,缺失位置传
undefined; - 不传组合函数:
zipWith(arr1, arr2)退化为基础zip,返回[[a1, b1], [a2, b2], ...]形式的元组数组; - 空输入:
zipWith(null)、zipWith(undefined)、zipWith()均返回[];zipWith(identity)(只传一个函数)也返回[]; - 组合函数参数完整性:测试验证了组合函数收到的是"各数组同一位置元素"的完整参数列表,例如
zipWith([1, 2], [3, 4], [5, 6], fn)中fn首次调用收到的参数为(1, 3, 5)。
总结
zipWith是处理"多数组按索引对齐并变换"场景的高频工具。在 es-toolkit 中:
- 追求 Lodash 行为对齐(
ArrayLike输入、缺省组合函数退化为zip、空值容错)时,使用es-toolkit/compat的兼容版,其内部基于unzip转置后map组合; - 追求性能与类型安全、且需要索引参数时,使用
es-toolkit/array的现代版; - 在函数式管道中,使用
es-toolkit/fp的柯里化版本。
如需进一步阅读,可对照现代版文档 docs/reference/array/zipWith.md,并参考unzip实现(src/array/unzip.ts)理解其转置逻辑。
【免费下载链接】es-toolkitA modern JavaScript utility library that's 2-3 times faster and up to 97% smaller, a major upgrade to lodash.项目地址: https://gitcode.com/GitHub_Trending/es/es-toolkit
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考