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 的 Lodash 兼容模块(es-toolkit/compat)中提供的高阶差集函数:它先用迭代器(iteratee)把每个元素转换为比较基准值,再从第一个数组中剔除在其他数组中"转换后值相同"的元素。本文以 docs/ja/compat/reference/array/differenceBy.md 为骨架,结合仓库源码与测试用例,讲解其完整用法、参数语义、实现原理与边界行为,读完即可在迁移 Lodash 或做数据清洗时直接上手。
一、函数定位与适用场景
const result = differenceBy(array, ...values, iteratee);differenceBy解决的核心问题是"按转换后的值做差集":直接比较原始值往往不符合业务语义,例如2.1与2.3在按Math.floor转换后都是2,应视为相同。此时用它就能以"某个属性、字符串长度或任意映射结果"为基准求差集,非常适合:
- 对象数组按特定属性去重/求差:如按
id比较两个用户列表; - 按派生值比较:如按字符串长度、按取整后的数值比较;
- 需要 Lodash 行为兼容的存量项目:函数签名、
-0归一化、NaN匹配等细节与 Lodash 保持一致。
需要特别注意的是,文档开头给出了明确的性能提示:兼容版differenceBy因复杂的参数处理和迭代器转换,运行较慢,建议优先使用 es-toolkit 原生的快速版 differenceBy(源码位于 src/array/differenceBy.ts)。兼容版的价值在于"行为兼容、无缝迁移",追求极致性能时则应切换到原生版本。
二、安装与导入
兼容版函数从es-toolkit/compat子路径导入:
import { differenceBy } from 'es-toolkit/compat';compat模块的完整入口定义在 src/compat/index.ts 与 src/compat/compat.ts,其设计目标是与 Lodash 保持 API 与行为兼容,便于项目无痛迁移。
三、核心用法详解
3.1 使用函数迭代器:按转换结果比较
最直接的形式是传入一个映射函数,先转换再比较:
import { differenceBy } from 'es-toolkit/compat'; // 小数点以下切り捨てで比較(按向下取整结果比较) differenceBy([2.1, 1.2], [2.3, 3.4], Math.floor); // Returns: [1.2] (Math.floor(2.1) === Math.floor(2.3) ため2.1を除外) // 说明:2.1 与 2.3 取整后同为 2,因此 2.1 被剔除,仅保留 1.2这里Math.floor同时作用于第一个数组的元素和所有待排除数组的元素,转换后值相等的元素从结果中剔除。
3.2 使用属性名迭代器:按属性比较
第二个参数可以传字符串属性名,此时会提取每个元素的该属性值作为比较基准:
import { differenceBy } from 'es-toolkit/compat'; // 按字符串长度比较 differenceBy(['one', 'two', 'three'], ['four', 'eight'], 'length'); // Returns: ['one', 'two'] // 说明:'three' 与 'eight' 长度都是 5,因此 'three' 被剔除 // 按对象 id 属性比较 const users1 = [ { id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }, ]; const users2 = [{ id: 1, name: 'Different Alice' }]; differenceBy(users1, users2, 'id'); // Returns: [{ id: 2, name: 'Bob' }] // 说明:id 为 1 的对象(即使 name 不同)被剔除属性名迭代器让"按对象数组的某个字段求差集"变得一行即可完成。
3.3 一次排除多个数组
...values支持任意数量的待排除数组,且每个数组都会先经迭代器转换再参与比较:
import { differenceBy } from 'es-toolkit/compat'; // 多个数组同时排除 differenceBy([2.1, 1.2, 3.5], [2.3], [1.4], [3.2], Math.floor); // Returns: [] (取整后 2、1、3 全部被覆盖,所有元素均被剔除) // 字符串数组按长度比较 differenceBy(['a', 'bb', 'ccc'], ['x'], ['yy'], ['zzz'], 'length'); // Returns: [] (长度 1、2、3 均被覆盖)从源码看,待排除数组会被拍平合并后再统一转换比较(见下文"实现原理")。
3.4 不传迭代器:退化为 difference
当最后一个参数是数组(而非函数、属性名等)时,differenceBy等价于普通差集:
import { differenceBy } from 'es-toolkit/compat'; // 不传迭代器 differenceBy([1, 2, 3], [2, 4]); // Returns: [1, 3]这一点在测试 src/compat/array/differenceBy.spec.ts 中也有验证:differenceBy([2, 1, 2, 3], [3, 4], [3, 2])返回[1]。
3.5 空值与空数组处理
null或undefined作为第一个数组时,一律返回空数组:
import { differenceBy } from 'es-toolkit/compat'; differenceBy(null, [1, 2], Math.floor); // Returns: [] differenceBy(undefined, [1, 2], x => x); // Returns: []对应实现见 src/compat/array/differenceBy.ts:入口处先通过isArrayLikeObject检查,非类数组对象直接返回[]。测试 src/compat/array/differenceBy.spec.ts 进一步证明字符串'23'作为首个参数也会返回[]。
四、参数与返回值规范
| 项目 | 类型 | 说明 |
|---|---|---|
array | ArrayLike<T> \| null \| undefined | 求差集的基准数组,可为类数组对象;非类数组或空值返回[] |
...values | Array<ArrayLike<T>> | 待排除元素所在的一个或多个数组 |
iteratee | ValueIteratee<T> | 将每个元素转换为比较值的迭代器,支持函数、属性名、属性值对或部分对象 |
| 返回值 | T[] | 剔除"转换后值相同"元素后的新数组,不修改原数组 |
其中ValueIteratee<T>的类型定义位于 src/compat/_internal/ValueIteratee.ts:
export type ValueIteratee<T> = ((value: T) => unknown) | (PropertyKey | [PropertyKey, any] | PartialShallow<T>);即迭代器可以是:函数、属性键(PropertyKey,即字符串/数字/symbol)、[属性键, 期望值]二元组、或部分对象。
五、迭代器转换机制(源码级原理)
兼容版differenceBy会把最后一个参数当作迭代器,交给统一的迭代器工厂createIteratee处理。工厂函数实现在 src/compat/util/iteratee.ts,转换规则如下:
| 传入的迭代器 | 转换结果 | 说明 |
|---|---|---|
null/undefined | identity | 原样返回输入,等价于不转换 |
| 函数 | 原函数 | 直接使用,如Math.floor |
二元数组['a', 1] | matchesProperty('a', 1) | 判断元素属性a是否等于1 |
| 对象 | matches(obj) | 判断元素是否匹配该部分对象 |
| 其他(属性键) | property(value) | 提取元素对应属性值,如'length'、'id' |
这解释了为什么'length'、'id'这类字符串能直接作为迭代器使用,也意味着兼容版支持 Lodash 风格的['a', 1]属性值对写法。配套的matchesProperty、matches、property实现分别位于 src/compat/predicate/matchesProperty.ts、src/compat/predicate/matches.ts、src/compat/object/property.ts。
六、兼容版实现流程与边界行为
6.1 主实现流程
兼容版函数体位于 src/compat/array/differenceBy.ts,执行步骤如下:
- 空值防护:
isArrayLikeObject(array)为假则直接返回[]; - 提取迭代器:
last(_values)取最后一个参数作为迭代器; - 拍平待排除数组:
flattenArrayLike(_values)将多个类数组参数拍平为单一数组,实现见 src/compat/_internal/flattenArrayLike.ts(跳过其中非类数组的值); - 判断分支:
- 若迭代器本身是类数组对象(说明没传迭代器),调用原生 difference 求普通差集;
- 否则调用原生
differenceBy并传入createIteratee(iteratee)转换后的迭代器;
- 结果归一化:对每个结果元素执行
normalizeZero,把-0归一为0,对齐 Lodash 行为(见 src/compat/_internal/normalizeZero.ts)。
6.2 底层原生实现:基于 Set 的 O(n) 算法
无论走哪个分支,最终都由 es-toolkit 原生实现完成核心计算。原生differenceBy(src/array/differenceBy.ts)的算法非常简洁高效:
- 对第二个数组逐元素应用
mapper,构建Set(利用 Set 的哈希查找); - 遍历第一个数组,对每个元素应用
mapper,若映射结果不在 Set 中则保留。
const mappedSecondSet = new Set(secondArr.map(item => mapper(item))); return firstArr.filter(item => { return !mappedSecondSet.has(mapper(item)); });由于Set.has是常数级查找,整体时间复杂度为 O(n),这也是原生版性能优于兼容版(兼容版额外承担了参数解析、数组拍平与迭代器转换开销)的根本原因。原生difference(src/array/difference.ts)采用同样的 Set 策略。
6.3 测试覆盖的边界行为
src/compat/array/differenceBy.spec.ts 完整覆盖了以下 Lodash 兼容语义,可作为行为契约参考:
-0归一化为0:differenceBy([-0, 1], [1])返回[0](L48-L56),并在显式传迭代器时同样生效(L128-L130);NaN匹配:differenceBy([1, NaN, 3], [NaN, 5, NaN])返回[1, 3],说明NaN能被正确识别为相同值(L58-L60);- 大型数组:以
LARGE_ARRAY_SIZE规模验证性能与正确性(L62-L74); arguments与类数组对象:{ 0: 1, 1: 2, length: 2 }这类类数组可直接作为基准或排除来源(L104-L117);- 非数组值过滤:排除参数中的非类数组值(如字符串
'2')会被跳过,不会破坏结果(L123-L126)。
6.4 类型重载
为了在 TypeScript 下精确推导返回类型,兼容版对 1 到 6 个排除数组分别声明了重载(T1~T6泛型),第 7 个起落入剩余参数重载,见 src/compat/array/differenceBy.ts。这意味着对T1[]与T2[]混合求差集时,返回类型始终是第一个数组的元素类型T1[]。
七、与原生 differenceBy 的选型建议
| 维度 | es-toolkit/compat兼容版 | es-toolkit 原生版 |
|---|---|---|
| 导入路径 | es-toolkit/compat | es-toolkit |
| 迭代器形式 | 函数 / 属性名 / 属性值对 / 部分对象 | 仅函数(mapper) |
| 多数组排除 | 支持任意数量 | 仅支持两个数组 |
-0→0归一化 | 支持(对齐 Lodash) | 不支持 |
| 空值 / 非类数组参数 | 安全返回[] | 由调用方自行保证 |
| 性能 | 较慢(参数解析 + 迭代器转换开销) | 更快(纯 Set 算法) |
结论:正在从 Lodash 迁移、需要严格行为兼容的代码,用es-toolkit/compat的differenceBy;新代码追求性能与简洁,直接用 es-toolkit 原生 differenceBy(对应源码 src/array/differenceBy.ts)。如需进一步了解兼容模块的整体设计,可阅读 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),仅供参考