news 2026/9/17 8:30:18

es-toolkit/fp 中的 differenceWith:用自定义相等函数在 pipe 管道中实现差集过滤

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
es-toolkit/fp 中的 differenceWith:用自定义相等函数在 pipe 管道中实现差集过滤

es-toolkit/fp 中的 differenceWith:用自定义相等函数在 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提供的differenceWith。它是一个数据后置(data-last)的管道算子:先用secondArrayareItemsEqual完成配置,返回一个等待数组输入的函数,再与pipe组合,在自左向右的数据流中按自定义相等规则剔除匹配项。读完本文,你将掌握differenceWith的参数契约、与普通版differenceWith的选用准则、它在pipe内触发惰性求值(lazy evaluation)与提前终止的底层原理,以及如何通过测试用例验证其行为。

概览:为 pipe 而生的差集算子

differenceWith创建一个函数,该函数使用自定义相等函数从管道传入的数组中排除与配置值相等的元素。它专用于函数式编程风格,典型用法是与pipe串联:

const result = pipe(array, differenceWith(secondArray, areItemsEqual));

es-toolkit/fp的定位中,每个 fp 函数都是"先传配置、后传数据"的形态:differenceWith(secondArray, areItemsEqual)先完成配置并返回(array) => T[]pipe再把数据逐个喂给这些算子,见 fp 入口文档。

选用建议:在不使用pipe拼接管道的普通代码中,推荐直接使用 es-toolkit 的普通版differenceWith。只有当你要通过pipe串联多个变换时,才使用这里的 fp 版本。

用法与语义

differenceWith仅当areItemsEqualsecondArray中的每一个值都返回false时,才保留被管道传入数组中的对应值。换句话说,它返回的是"与比较数组没有任何元素相等"的子集。

import { differenceWith, pipe } from 'es-toolkit/fp'; pipe( [{ id: 1 }, { id: 2 }], differenceWith([{ id: 2 }], (a, b) => a.id === b.id) ); // => [{ id: 1 }]

上例中,{ id: 2 }与比较数组中的{ id: 2 }id相等,因此被剔除,最终结果只剩[{ id: 1 }]

参数(Parameters)

参数类型说明
secondArrayreadonly U[]用于与管道输入逐项比较、从而决定排除哪些值的数组
areItemsEqual(item: T, other: U) => boolean判定两个值是否相等的自定义比较函数

返回值(Returns)

(array: readonly T[] => T[]):一个将readonly T[]映射为"未被比较函数匹配到的值"组成的新数组的函数。

从普通版到 fp 版:实现与复用

fp 版differenceWith并没有重新发明算法,而是复用了 es-toolkit 普通版differenceWith的实现。源码见 src/fp/array/differenceWith.ts:

export function differenceWith<T, U>( secondArray: readonly U[], areItemsEqual: (item: T, other: U) => boolean ): (array: readonly T[]) => T[] { function differenceWithEager(array: readonly T[]): T[] { return differenceWithToolkit(array, secondArray, areItemsEqual); } const differenceWithLazy = createLazyFunction<T, T>((value, _index, emit) => { if (secondArray.every(other => !areItemsEqual(value, other))) { emit(value); } }); return combineEagerAndLazyFunctions(differenceWithEager, differenceWithLazy); }

这段代码揭示了三个关键点:

  1. eager 路径复用普通实现differenceWithEager内部直接调用src/array/differenceWith.ts导出的differenceWith,签名变化只是把(firstArr, secondArr, areItemsEqual)柯里化成了(secondArray, areItemsEqual)(array)
  2. lazy 路径按元素推送differenceWithLazycreateLazyFunction构造逐元素变换,每个元素只有通过secondArray.every(other => !areItemsEqual(value, other))的检查才会被emit推送到下游。
  3. 两者被combineEagerAndLazyFunctions合并:返回的函数在直接调用时行为与 eager 完全一致;放进pipe后,附加的lazy变换让pipe可以将其与相邻惰性函数熔合为单趟遍历。

普通版differenceWith的算法本身非常直接(src/array/differenceWith.ts):

export function differenceWith<T, U>( firstArr: readonly T[], secondArr: readonly U[], areItemsEqual: (x: T, y: U) => boolean ): T[] { return firstArr.filter(firstItem => { return secondArr.every(secondItem => { return !areItemsEqual(firstItem, secondItem); }); }); }

它等价于:对firstArr中的每个元素firstItem,仅当secondArr不存在任何一个使areItemsEqual(firstItem, secondItem)为真的secondItem时保留。从源码结构看,其时间复杂度为 O(n×m)(n 为第一个数组长度,m 为第二个数组长度),比较函数每次被调用的次数等于两数组长度的乘积上界。

惰性求值:为什么在 pipe 里更快

es-toolkit/fp的核心卖点是管道熔合(pipeline fusion)。当pipe中连续出现可惰性函数(mapfiltertakedifferenceWith等)时,pipe会把它们熔合成一趟遍历:每个元素一次性穿过所有步骤,而不是每步都生成中间数组;一旦末尾的take(n)收集够结果,整趟遍历立即停止,剩余输入不再被访问。完整机制见 src/fp/pipe.ts 与惰性原语 src/fp/_internal/lazy.ts。

createLazyFunction实现的是一种"推送式"(push)管道:每个函数接收下游的emit(即 sink),再把值推入其中;take这类可提前终止的函数通过返回false立即停住驱动循环,无需生成器挂起/恢复,也没有逐元素的迭代器对象开销。

测试如何验证提前终止

differenceWith的惰性行为有专门的测试用例,见 src/fp/array/differenceWith.spec.ts:

it('supports lazy evaluation with a short-circuiting operator', () => { const spy = vi.fn((item: { id: number }) => item); expect( pipe( [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }], map(spy), differenceWith([1], (item, id) => item.id === id), take(2) ) ).toEqual([{ id: 2 }, { id: 3 }]); expect(spy).toHaveBeenCalledTimes(3); });

这个测试用vi.fn间谍函数记录了map的回调调用次数:输入有 4 个元素,但spy只被调用了3 次。原因正是熔合与提前终止——take(2)在收集到 2 个结果后立即中断整趟遍历,第 4 个元素{ id: 4 }从未被访问。若按 eager 逐数组执行,map必然对全部 4 个元素各调用一次。

与 difference / differenceBy 的对比

es-toolkit/fp的数组模块同时提供了三个差集算子,见 src/fp/array/index.ts 的导出:

  • difference(secondArray):基于 Set 与 SameValueZero 相等语义做差集。fp 实现(src/fp/array/difference.ts)在配置阶段就构建new Set(secondArray),惰性变换用!secondSet.has(value)判断,查找为 O(1),适合处理原始值数组。
  • differenceBy(secondArray, getDiffValue):先用映射函数把两数组元素变换成可比较键再做差集,适合"按某字段去重"的场景。
  • differenceWith(secondArray, areItemsEqual):自由度最高,比较逻辑完全由你提供的areItemsEqual决定,适合对象数组按业务规则(如按id、按name忽略年龄差异)比较,也可比较类型不同的数组。

三者共享同一套"eager 复用普通实现 + lazy 逐元素变换 +combineEagerAndLazyFunctions合并"的结构,因此在pipe中都能参与惰性熔合。

实战:对象数组按业务键过滤

结合普通版文档(docs/reference/array/differenceWith.md)中的示例,我们可以在pipe中实现"忽略次要字段差异"的过滤。例如,用户名单中 Alice 的年龄从 30 变为 31,但我们仍认为她是同一用户,需要把users2中出现的用户从users1中剔除:

import { differenceWith, pipe } from 'es-toolkit/fp'; const users1 = [ { name: 'Alice', age: 30 }, { name: 'Bob', age: 25 }, { name: 'Charlie', age: 35 }, ]; const users2 = [ { name: 'Alice', age: 31 }, // 年龄不同但仍是同一用户 { name: 'David', age: 25 }, ]; pipe( users1, differenceWith(users2, (a, b) => a.name === b.name) ); // => [{ name: 'Bob', age: 25 }, { name: 'Charlie', age: 35 }]

areItemsEqual只比较name,因此Alice(即使 age 不同)被排除,David不在users1中所以不影响结果。当这个differenceWith前后还串联了mapfiltertake等算子时,整条管道会以单趟、可提前终止的方式执行。

小结

  • differenceWith(secondArray, areItemsEqual)返回一个数据后置函数,仅在areItemsEqualsecondArray全部元素都为false时保留输入元素;
  • 它是 fp 版柯里化封装,eager 路径直接复用普通版differenceWith,lazy 路径用createLazyFunction实现逐元素推送;
  • pipe中它与相邻惰性函数熔合为单趟遍历,配合take等短路算子可实现提前终止(测试用 spy 断言map仅调用 3 次即验证了这一点);
  • 普通代码请用es-toolkit/array的普通版;需要按自定义规则(对象字段、跨类型比较)做差集时,fp 版differenceWithpipe管道中的正确选择。

【免费下载链接】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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/17 8:30:11

基于图像处理与机器学习的水浑浊度预测系统实现

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/17 8:27:25

大模型私有化部署显存优化技术与实践

1. 项目背景与核心挑战在人工智能技术快速发展的当下&#xff0c;大模型私有化部署面临着一个关键瓶颈——显存资源的高消耗。以当前主流的百亿参数模型为例&#xff0c;单次推理任务通常需要占用16GB以上的显存容量&#xff0c;而训练过程更是需要多块高端显卡并行工作。这种资…

作者头像 李华
网站建设 2026/9/17 8:25:52

Android离线中文TTS集成:espeak-ng从交叉编译到JNI封装

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/17 8:23:26

独立游戏地编的AI辅助实践:概念锚定与可平铺材质

做独立游戏的第五个年头&#xff0c;我最怕的早就不是写不出代码&#xff0c;而是地编永远铺不完。你搭好一套玩法原型&#xff0c;代码跑得挺顺&#xff0c;一到要往场景里填东西&#xff0c;工作量就像开了闸&#xff1a;地形、材质、植被、石头、光照、氛围&#xff0c;一层…

作者头像 李华
网站建设 2026/9/17 8:22:49

PentAGI实战:大模型驱动的Linux自主智能体架构与部署解析

上个月给一个内部项目做自动化巡检&#xff0c;想在开源社区里找个能自动操作 Linux 环境的智能体&#xff0c;翻来翻去看到了一个叫 PentAGI 的仓库。名字起得很直白&#xff1a;Pent 取自 penguin&#xff08;企鹅&#xff09;&#xff0c;AGI 是通用人工智能的缩写&#xff…

作者头像 李华