es-toolkit differenceBy 使用指南:基于映射键的数组差集计算
2026/9/17 1:39:38 网站建设 项目流程

es-toolkit differenceBy 使用指南:基于映射键的数组差集计算

【免费下载链接】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

differenceBy是 es-toolkit 数组工具集中用于计算差集(difference)的函数:它先通过一个转换函数(mapper)将两个数组的每个元素映射为比较键,再返回"仅存在于第一个数组、且映射键未在第二个数组中出现"的元素组成的新数组。本文基于 docs/ja/reference/array/differenceBy.md 展开,结合 核心实现、单元测试 以及 fp / compat 变体源码,完整讲解其 API 签名、典型用法、底层原理与边界行为,帮助你用它解决"按特定基准求差集"的实际问题。

一、为什么需要 differenceBy

原生的filter+includes只能做全等比较。当两个数组的元素是对象、或需要按某个字段/计算值来判定"是否相同"时,直接比较行不通。例如:

  • 两个对象数组,想按id字段判断重复;
  • 数字数组与对象数组混排,想统一按某种规则归一化后比较;
  • 字符串数组想按"长度"而非"内容"比较。

differenceBy正是为这类"按自定义基准求差集"的场景设计的:它接收一个mapper转换函数,把两个数组的元素都映射成可比较的键,再执行差集运算。与之相对,只做全等比较的简化版是 difference,它不接收 mapper,直接以元素自身为键计算差集。

二、API 签名与参数说明

函数签名如下(摘自 src/array/differenceBy.ts):

export function differenceBy<T, U>( firstArr: readonly T[], secondArr: readonly U[], mapper: (value: T | U) => unknown ): T[];

参数

参数类型说明
firstArrT[]readonly T[]差集计算的基准数组,返回结果中的元素都来自该数组
secondArrU[]readonly U[]包含要从第一个数组中排除的元素的数组
mapper(value: T \| U) => unknown将两个数组的元素映射为比较键的函数;两个数组的元素都会经过它转换,以转换后的值为基准进行比较

返回值

T[]:一个新数组,包含"按映射值判断仅存在于第一个数组"的元素。原始数组不会被修改,映射值相等的元素会从结果中被排除。

几个值得注意的类型细节:

  • 两个数组的元素类型可以不同(TU),因此mapper的入参类型是联合类型T | U——这是它支持"不同类型数组互相比较"的关键;
  • 两个参数声明为readonly T[]/readonly U[],意味着你可以直接传入由as const产生的只读数组,无需拷贝;
  • mapper的返回值类型是unknown,比较基于Set的严格相等语义(见下文"实现原理")。

三、典型用法示例

1. 对象数组按 id 求差集

import { differenceBy } from 'es-toolkit/array'; const array1 = [{ id: 1 }, { id: 2 }, { id: 3 }]; const array2 = [{ id: 2 }, { id: 4 }]; differenceBy(array1, array2, item => item.id); // 返回值: [{ id: 1 }, { id: 3 }] // id 为 2 的元素同时存在于两个数组中,因此被排除。

2. 不同类型数组互相比较

当两个数组元素类型不一致时,可以在mapper内部做类型归一化:

const objects = [{ id: 1 }, { id: 2 }, { id: 3 }]; const numbers = [2, 4]; differenceBy(objects, numbers, item => (typeof item === 'object' ? item.id : item)); // 返回值: [{ id: 1 }, { id: 3 }] // numbers 中的 2 经映射后与 objects 中 id 为 2 的元素键相等,故被排除。

这正是泛型签名mapper: (value: T | U) => unknown的用武之地:item可能是对象也可能是数字,通过typeof分支统一映射到同一键空间。

3. 按字符串长度求差集

const words1 = ['apple', 'banana', 'cherry']; const words2 = ['grape', 'lemon']; differenceBy(words1, words2, word => word.length); // 返回值: ['banana', 'cherry'] // 'apple' 的长度为 5,与 'grape'(5)、'lemon'(5)相同,因此被排除。

4. 配合内置函数作为 mapper

mapper也可以是任何普通函数,比如Math.floor(在 单元测试 中即有该用法):

differenceBy([1.2, 2.3, 3.4], [1.2], Math.floor); // Math.floor 后: 1.2 -> 1, 2.3 -> 2, 3.4 -> 3;第二数组 1.2 -> 1 // 返回值: [2.3, 3.4]

四、实现原理:Set 驱动的 O(n) 差集

核心实现非常精简,只有两个步骤(src/array/differenceBy.ts):

const mappedSecondSet = new Set(secondArr.map(item => mapper(item))); return firstArr.filter(item => { return !mappedSecondSet.has(mapper(item)); });

其设计要点如下:

  1. 第二数组只映射一次:先用mapsecondArr的全部元素经mapper转换,装入一个Set,构造"排除键集合"。这一步时间复杂度为 O(m)(m 为第二数组长度),且mapper对第二数组的每个元素恰好调用一次
  2. 第一数组逐个过滤:遍历firstArr,对每个元素调用一次mapper,用Set.has在常数时间内判断该键是否命中排除集合。这一步时间复杂度为 O(n)(n 为第一数组长度)。
  3. 整体复杂度 O(n + m):相比filter+includes(后者是 O(n×m) 的嵌套扫描),Set方案在大数组场景下有数量级的性能优势,这也是 es-toolkit 强调性能的实现风格之一。
  4. 比较语义Set.has使用 SameValueZero 严格相等语义(1'1'不相等,NaNNaN相等),因此 mapper 返回的键必须是"可严格比较"的值,例如数字、字符串或原始引用。

与 difference 的关系

不带 mapper 的 difference 是differenceBy的特例:

export function difference<T>(firstArr: readonly T[], secondArr: readonly T[]): T[] { const secondSet = new Set(secondArr); return firstArr.filter(item => !secondSet.has(item)); }

可以看到两者结构完全同构:difference相当于mapper为恒等函数(x => x)时的differenceBy。如果你只需要全等比较,直接用difference更简洁;需要自定义基准时再升级为differenceBy

五、函数式编程变体:fp 模块下的可组合用法

es-toolkit 的fp子模块提供了柯里化、可组合的differenceBy(src/fp/array/differenceBy.ts),签名从"两数组 + mapper"变为"先传排除数组与 mapper,再传数据数组":

import { differenceBy, pipe } from 'es-toolkit/fp'; pipe( [{ id: 1 }, { id: 2 }], differenceBy([2], value => (typeof value === 'number' ? value : value.id)) ); // => [{ id: 1 }]

从源码结构看,该变体做了两件事:

  • 预计算排除集合:在工厂函数阶段就把secondArray映射为Set并闭包保存,之后每次对数组求差集都直接复用;
  • 惰性求值支持:通过_internal/lazy.ts中的createLazyFunctioncombineEagerAndLazyFunctions组合出"惰性优先、必要时回退到急切执行"的函数,使其在pipe链中可与后续步骤合并遍历,减少中间数组分配。

如果数据流是"先定义差集规则、再批量作用于多个数组",fp 变体可以避免重复传入排除数组与 mapper,并能减少一次映射开销。

六、lodash 兼容变体:compat 模块的多数组支持

compat子模块提供了与 lodash 行为对齐的differenceBy(src/compat/array/differenceBy.ts),能力更广:

  • 支持多个排除数组differenceBy(array, values1, values2, ..., iteratee),所有排除数组会被扁平化后统一参与比较(见 flattenArrayLike 的使用);
  • iteratee 简写:通过createIteratee(src/compat/util/iteratee.ts)支持 lodash 风格的简写形式,例如传入属性名字符串表示value => value[prop]、传入对象表示按对象匹配;
  • 容忍空值与类数组array参数允许为null/undefined(此时返回[]),也接受ArrayLike(如字符串、类数组对象),内部先做isArrayLikeObject校验再Array.from转换;
  • -0 归一化:结果统一经过normalizeZero,把-0规整为0,对齐 lodash 的输出细节。

需要与 lodash 行为严格一致的场景(尤其是多排除数组 + iteratee 简写)应选用compat变体;标准 es-toolkit 的differenceBy则保持"两数组 + mapper 函数"的纯粹形态。

七、边界情况与测试佐证

src/array/differenceBy.spec.ts 用 vitest 覆盖了核心行为,可作为行为契约参考:

  1. 正常差集differenceBy([1.2, 2.3, 3.4], [1.2], Math.floor)返回[2.3, 3.4]——1.2映射为1后与第二数组冲突而被排除。
  2. 空的第一数组differenceBy([], [1.2], Math.floor)返回[]——空输入恒为空输出,实现上由filter天然保证。
  3. 不同类型数组CSV[]JSON[]两个结构不同的对象数组,按value.id映射后正确返回仅存在于第一数组的元素[{ id: 1 }, { id: 3 }]

从实现推演出的其他边界行为:

  • 映射键重复:第一数组中多个元素映射到同一键时,只要该键不在排除集合中,它们会全部保留Set只负责"命中即排除");
  • 引用类型键:若 mapper 返回对象/数组等引用值,Set.has按引用比较,通常应让 mapper 返回原始值(number / string)以确保比较符合直觉;
  • 不修改原数组:函数返回新数组,firstArrsecondArr均保持原样,适合函数式数据流。

八、安装与导入方式

es-toolkit 支持按路径导入以获取最佳 tree-shaking 效果(导出声明):

// 推荐:按子路径导入,减少打包体积 import { differenceBy } from 'es-toolkit/array'; // 或从主入口导入 import { differenceBy } from 'es-toolkit'; // 函数式变体 import { differenceBy } from 'es-toolkit/fp'; // lodash 兼容变体(支持多数组与 iteratee 简写) import { differenceBy } from 'es-toolkit/compat';

所有变体均内置 TypeScript 类型推导:当两个数组元素类型不同时,mapper的入参会被推导为联合类型T | U,配合typeof分支即可安全完成类型归一化。

总结

differenceBy是"按映射键求差集"的通用解决方案:mapper决定比较基准(字段、归一化结果、计算值均可),Set保证 O(n + m) 的时间复杂度,返回值是全新的、仅含第一数组元素的数组。日常开发中,对象按 id 去重、跨类型数组比较、按长度/格式化结果求差等场景均可直接套用;需要流水线式组合时使用fp变体,需要 lodash 级兼容能力(多数组、iteratee 简写)时使用compat变体。相关实现、测试与文档可从 核心实现、单元测试 与 官方文档 继续深入阅读。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询