es-toolkit/fp 的 intersectionWith:用自定义比较函数在 pipe 管道中做交集运算
【免费下载链接】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 的函数式编程入口es-toolkit/fp提供了一种数据后置(data-last)的intersectionWith变体:先用自定义等值函数配置好比较逻辑,得到一个等待数据的函数,再放进pipe管道中,以自上而下的顺序处理数组。本文以 docs/ja/fp/reference/intersectionWith.md 为骨架,结合源码实现与测试用例,讲清它的参数语义、惰性求值原理、与普通版intersectionWith的差异,以及实际可复制的用法。
一、它解决什么问题
es-toolkit/array中的普通intersectionWith接受三个参数:intersectionWith(firstArr, secondArr, areItemsEqual)。而在函数式编程风格中,我们希望把"筛选两个数组的交集"当作管道中的一步,与其他变换(map、filter、take等)串联起来。
于是es-toolkit/fp版的intersectionWith被设计成分两段调用:第一段接收secondArray与areItemsEqual(配置阶段),第二段接收被管道传入的数组(数据处理阶段):
const result = pipe(array, intersectionWith(secondArray, areItemsEqual));它返回一个新函数:把输入的readonly T[]映射为"与secondArray中至少一个值满足areItemsEqual相等判定"的元素数组。
二、基本用法
在pipe中,intersectionWith保留那些对secondArray中至少一个值令areItemsEqual返回true的管道数组元素:
import { intersectionWith, pipe } from 'es-toolkit/fp'; pipe( [{ id: 1 }, { id: 2 }], intersectionWith([{ id: 2 }], (a, b) => a.id === b.id) ); // => [{ id: 2 }]这里areItemsEqual按id属性比较对象,因此只有{ id: 2 }保留下来。该示例与 src/fp/array/intersectionWith.spec.ts 中的测试用例完全一致。
参数
secondArray(readonly U[]):包含比较目标值的数组,作为"参照集合"。areItemsEqual((item: T, other: U) => boolean):判断两个值是否相等的函数,返回true表示相等。
返回值
(array: readonly T[]) => T[]:一个把readonly T[]转换为"与比较函数匹配的值数组"的函数。也就是说,intersectionWith(secondArray, areItemsEqual)本身就是一个等待数据的一元函数,天然适配pipe。
三、源码级解析:Eager + Lazy 双实现
从源码结构看,src/fp/array/intersectionWith.ts 同时准备了两套行为,并用内部工具组合它们:
export function intersectionWith<T, U>( secondArray: readonly U[], areItemsEqual: (item: T, other: U) => boolean ): (array: readonly T[]) => T[] { function intersectionWithEager(array: readonly T[]): T[] { return intersectionWithToolkit(array, secondArray, areItemsEqual); } const intersectionWithLazy = createLazyFunction<T, T>((value, _index, emit) => { if (secondArray.some(other => areItemsEqual(value, other))) { emit(value); } }); return combineEagerAndLazyFunctions(intersectionWithEager, intersectionWithLazy); }Eager 实现:直接复用
es-toolkit/array的intersectionWith。其核心逻辑用filter+some表达——对firstArr的每个元素,检查secondArr中是否存在一个元素令areItemsEqual返回true:return firstArr.filter(firstItem => { return secondArr.some(secondItem => { return areItemsEqual(firstItem, secondItem); }); });Lazy 实现:用
createLazyFunction构造一个逐元素(push 式)变换。对每个输入值,若secondArray中存在某个other令areItemsEqual(value, other)为真,就把该值emit给下一级管道。组合:
combineEagerAndLazyFunctions(见 src/fp/_internal/lazy.ts)把 eager 函数原样返回,并挂上lazy元数据。于是:直接调用时行为与普通版完全一致;放进pipe时,pipe可以识别其惰性变换并与其他惰性函数融合。
四、惰性求值:为什么它适合放在管道中
pipe的融合机制(详见 docs/fp/intro.md 与 docs/fp/reference/pipe.md)把相邻的惰性函数(map、filter、take、intersectionWith等)合并为单趟遍历:不再每步都产生中间数组,而是让每个元素一次性穿过所有阶段;当尾部的短路函数(如take)收集到足够结果时,整个遍历立即停止,后面的输入不再被访问。
intersectionWith的惰性变换在 src/fp/array/intersectionWith.spec.ts 中有明确的测试证据:
const spy = vi.fn((item: { id: number }) => item); expect( pipe( [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }], map(spy), intersectionWith([3], (item, id) => item.id === id), take(1) ) ).toEqual([{ id: 3 }]); expect(spy).toHaveBeenCalledTimes(3);注意spy只被调用了3 次,而不是 4 次:take(1)收集到第一个结果({ id: 3 })后管道立即短路,{ id: 4 }根本不会被map处理。这正是"融合 + 短路"带来的收益——数据规模越大、take越早满足,节省的遍历就越多。
五、实战扩展:自定义比较函数的典型场景
借助自定义比较函数,intersectionWith可以处理普通===无法胜任的复杂对象。以下场景来自普通版intersectionWith文档,同样适用于fp版(只需把调用方式改为管道形式):
按对象属性比较
import { pipe } from 'es-toolkit/fp'; import { intersectionWith } from 'es-toolkit/fp'; const users1 = [ { id: 1, name: 'john' }, { id: 2, name: 'jane' }, ]; const users2 = [ { id: 2, name: 'jane' }, { id: 3, name: 'bob' }, ]; pipe(users1, intersectionWith(users2, (a, b) => a.id === b.id)); // => [{ id: 2, name: 'jane' }]比较不同类型的值
泛型签名areItemsEqual: (item: T, other: U) => boolean允许两数组元素类型不同:
const objects = [ { id: 1, name: 'apple' }, { id: 2, name: 'banana' }, ]; const ids = [2, 3]; pipe(objects, intersectionWith(ids, (obj, id) => obj.id === id)); // => [{ id: 2, name: 'banana' }]大小写不敏感的字符串比较
const words1 = ['Apple', 'Banana']; const words2 = ['apple', 'cherry']; pipe(words1, intersectionWith(words2, (a, b) => a.toLowerCase() === b.toLowerCase())); // => ['Apple']数值容差比较
const numbers1 = [1.1, 2.3, 3.7]; const numbers2 = [1.0, 2.5, 4.0]; pipe(numbers1, intersectionWith(numbers2, (a, b) => Math.abs(a - b) < 0.5)); // => [1.1, 2.3, 3.7]注意:返回结果始终来自被管道传入的第一个数组(保留其原始元素与顺序),secondArray只作为判定依据。
六、与普通版如何选择
es-toolkit/fp版并不是独立实现的另一套算法,而是对es-toolkit/array版的重封装,仅改变调用形态。选择建议如下:
- 普通代码中直接调用:使用原版
intersectionWith,即intersectionWith(firstArr, secondArr, areItemsEqual),更直观、无额外间接层。 - 需要用
pipe串联多个变换时:使用本fp版,把intersectionWith(secondArray, areItemsEqual)作为管道中的一步,享受惰性融合带来的单趟遍历与提前短路收益。 - 迁移 Lodash 调用点:可参考
es-toolkit/compat入口(见 docs/fp/intro.md 的说明)。
七、相关资源
- 文档:docs/ja/fp/reference/intersectionWith.md、英文版 docs/fp/reference/intersectionWith.md、普通版 docs/reference/array/intersectionWith.md
- 管道入口:docs/fp/reference/pipe.md,fp 模块总览:docs/fp/intro.md
- 源码:src/fp/array/intersectionWith.ts、底层普通版 src/array/intersectionWith.ts、惰性求值原语 src/fp/_internal/lazy.ts
- 测试:src/fp/array/intersectionWith.spec.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),仅供参考