news 2026/9/17 3:04:02

es-toolkit 函数式编程 uniqWith 详解:在 pipe 流水线中用自定义相等函数高效去重

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
es-toolkit 函数式编程 uniqWith 详解:在 pipe 流水线中用自定义相等函数高效去重

es-toolkit 函数式编程 uniqWith 详解:在 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 的fp(函数式编程)子模块中的uniqWith为核心,讲解如何用它配合pipe完成基于自定义相等函数的去重。读完本文,你将掌握uniqWith的>const result = pipe(array, uniqWith(areItemsEqual));

二、API 签名与类型

fpuniqWith定义于 src/fp/array/uniqWith.ts,签名如下:

export function uniqWith<T>( areItemsEqual: (item: T, other: T) => boolean ): (array: readonly T[]) => T[]
项目说明
参数areItemsEqual(item: T, other: T) => boolean,用于判定两个值是否相等的函数。返回true表示视为重复,false表示视为不同。
返回值(array: readonly T[]) => T[],一个把readonly T[]按自定义相等基准去重后映射为新数组的函数。

对比普通版 uniqWith 的uniqWith(arr, areItemsEqual)fp版只是把参数顺序反转、将数组参数延迟到最后一步,这正是>import { pipe, uniqWith } from 'es-toolkit/fp'; pipe( [{ id: 1 }, { id: 1 }, { id: 2 }], uniqWith((a, b) => a.id === b.id) ); // => [{ id: 1 }, { id: 2 }]

由于uniqWith((a, b) => a.id === b.id)返回的是"接收数组"的函数,它可以直接放在pipe的任意位置,与mapfiltertake等变换自由组合:

import { filter, map, pipe, take, uniqWith } from 'es-toolkit/fp'; pipe( [ { id: 1, score: 60 }, { id: 1, score: 60 }, { id: 2, score: 85 }, { id: 3, score: 40 }, ], uniqWith((a, b) => a.id === b.id), map(item => item.score), filter(score => score >= 60), take(2) ); // => [60, 85]

比较函数并不局限于"字段相等"这种朴素场景。参考普通版文档 docs/reference/array/uniqWith.md 与源码注释中的示例,可以构造各种自定义相等语义:

// 差值小于 1 视为相等 const numbers = [1.2, 1.5, 2.1, 3.2, 5.7, 5.3, 7.19]; pipe( numbers, uniqWith((a, b) => Math.abs(a - b) < 1) ); // => [1.2, 3.2, 5.7, 7.19] // 多个字段同时相等才视为重复 const products = [ { name: 'iPhone', brand: 'Apple', price: 1000 }, { name: 'iPhone', brand: 'Apple', price: 1100 }, // name 与 brand 相同,视为重复 { name: 'Pixel', brand: 'Google', price: 800 }, ]; pipe( products, uniqWith((a, b) => a.name === b.name && a.brand === b.brand) ); // => 保留第一项 iPhone 与 Pixel

四、源码级解析:eager 与 lazy 的双轨实现

fpuniqWith的实现非常有代表性,它通过combineEagerAndLazyFunctions把一个eager(即时)实现和一个lazy(惰性)变换打包进同一个函数(src/fp/array/uniqWith.ts):

export function uniqWith<T>(areItemsEqual: (item: T, other: T) => boolean): (array: readonly T[]) => T[] { function uniqWithEager(array: readonly T[]): T[] { return uniqWithToolkit(array, areItemsEqual); } const uniqWithLazy = (emit: Sink<T>): Sink<T> => { const seen: T[] = []; return (value: T): boolean => { if (seen.some(item => areItemsEqual(item, value))) { return true; } seen.push(value); return emit(value); }; }; return combineEagerAndLazyFunctions(uniqWithEager, uniqWithLazy); }

这里有两个关键细节:

  1. eager 路径直接复用普通版实现uniqWithEager委托给 src/array/uniqWith.ts,后者用双层循环实现:对每个新元素,用result.every(v => !areItemsEqual(v, item))与已保留的所有元素逐一比较,不重复才 push 进结果数组。可以推断该算法最坏情况下的时间复杂度为 O(n²),在数据量极大时需要注意。

  2. lazy 路径维护一个seen缓存数组。对于流经的每个元素,先检查它是否与seen中任一已保留元素相等;相等则直接返回true(表示"继续推送,但本元素丢弃"),不相等则加入seen并调用emit(value)把值传递给下一级。

五、惰性求值原理:pipe 如何融合 uniqWith

combineEagerAndLazyFunctions来自 src/fp/_internal/lazy.ts,它做的只是Object.assign(eager, { lazy, shortCircuit })——把 lazy 变换以元数据形式挂到函数对象上。pipe在运行时读取这些元数据来决定执行策略(src/fp/pipe.ts):

  • 先用chunkFunctions把连续出现的惰性函数(带lazy元数据)切成一个个"惰性组";
  • 当一组函数全部可惰性、且输入是可迭代对象、并且(组内有shortCircuit函数或输入不是数组)时,走lazyPipe融合路径;
  • 否则,逐个函数按普通方式依次应用。

lazyPipe(src/fp/pipe.ts)采用"push"式管道:从最后一个函数开始,把每个函数的 lazy 变换反向包成一个 sink,最终形成一个从源头接收元素、向下游逐级 emit 的链条,然后用一个循环把输入数组逐元素"推"进去。uniqWith的 lazy 变换就处在这个链条的某一环:上游推来的每个元素先经过它内部的seen去重,再把不重复的值 emit 给下游。

这种设计带来两个实际收益:

  • 无需中间数组map → uniqWith → filter这类连续惰性变换会融合为单趟遍历,而不是每步生成一个新数组;
  • 支持提前终止:当组内存在take这类shortCircuit函数时,它返回false会立刻中断驱动循环,uniqWith之前的所有函数都不会再处理剩余输入。

下面的测试用例(src/fp/array/uniqWith.spec.ts)用 spy 精确验证了这一点:

it('supports lazy evaluation with a short-circuiting operator', () => { const spy = vi.fn((item: { id: number }) => item); expect( pipe( [{ id: 1 }, { id: 1 }, { id: 2 }, { id: 3 }], map(spy), uniqWith((a, b) => a.id === b.id), take(2) ) ).toEqual([{ id: 1 }, { id: 2 }]); expect(spy).toHaveBeenCalledTimes(3); // 第 4 个元素从未被 map 处理 });

输入有 4 个元素,但map只被调用了 3 次——因为uniqWith去重后只剩{id:1}{id:2}两个值,take(2)在拿到第二个值后立即短路,{ id: 3 }甚至没有被map访问。这是"惰性 + 短路"融合的直接证据。

六、行为要点与使用建议

综合文档与源码,使用fpuniqWith时有几点值得注意:

  1. 保留首次出现的元素:原文档与源码注释都确认"每个相等组中第一个值被保留"。这保证了结果顺序与输入顺序一致。
  2. 比较函数应具备一致性areItemsEqual在 eager 路径中被反复调用(每个新元素 vs 已保留元素),其返回值应当稳定且对称,否则去重结果会依赖遍历顺序。
  3. eager 与 lazy 行为等价:直接调用uniqWith(areItemsEqual)(array)(不走 pipe)时,走的是 eager 路径,结果与普通版完全一致;只有放进pipe并满足融合条件时才会启用 lazy 路径。
  4. 与兄弟函数的取舍:如果只是想按某个键去重,uniqBy更合适;如果元素本身可用严格相等比较,uniq更快;只有当"相等"需要自定义规则(如模糊匹配、多字段组合、忽略大小写)时,才应使用uniqWith。三者同属 src/fp/array 目录,可对照阅读。
  5. 复杂度提醒:无论 eager 还是 lazy 路径,去重逻辑都依赖与"已保留集合"的线性比较,最坏为 O(n²)。处理超大规模数组时,可考虑先用uniqBy(键值可哈希化)做粗过滤。

七、总结

fp版的uniqWith是理解 es-toolkit 函数式编程子模块的一个绝佳切片:它展示了 contenteditable="false">【免费下载链接】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 3:03:57

ADC与DMA协同工作:高效电压采样方案的原理与实践

/* 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 3:03:48

GitHub Copilot替代方案深度实测:免费平替到本地部署一次讲清

/* 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 3:03:35

RoboMaster硬件实战指南:PCB设计与调试避坑手册

/* 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 3:03:29

CXL设备中Non-CXL Function的DVSEC映射机制解析

1. 这不是“普通PCIe配置空间”——CXL设备中Non-CXL Function MAP DVSEC的定位本质你拆开一块支持CXL的加速卡&#xff0c;用lspci -vvv扫一遍&#xff0c;看到一长串Capability结构&#xff1a;Vendor ID、MSI-X、AER、ACS……最后在某个Function里突然冒出一段叫DVSEC&#…

作者头像 李华
网站建设 2026/9/17 3:03:01

打印机共享失败排查:0x00000709、SMB与RPC错误码解析

打印机共享这四个字&#xff0c;看着平平无奇&#xff0c;真上手能把人磨到没脾气。我这些年帮朋友、帮公司行政处理过的打印机问题&#xff0c;没有一百也有八十起了&#xff1a;主机的打印机明明共享出去了&#xff0c;隔壁工位的电脑就是搜不到&#xff1b;昨天还打得欢&…

作者头像 李华