概述
differenceWith
是 Lodash 中的一个数组方法,它类似于 _.difference
,但允许我们提供一个自定义比较器函数来判断元素是否应该被排除。它会返回一个新数组,包含存在于第一个数组中但不存在于其他数组中的元素,使用指定的比较器函数进行元素比较。
前置学习
依赖函数
- baseRest:将一个函数转换为支持剩余参数的函数
- last:获取数组中的最后一个元素
- isArrayLikeObject:检查一个值是否为类数组对象
- baseDifference:计算数组差集的基础实现
- baseFlatten:扁平化数组的基础实现
技术知识
- 高阶函数:函数作为参数传递和返回
- 类数组对象:JavaScript 中类数组对象的概念和处理
- 自定义比较器:如何实现和使用自定义比较逻辑
- Rest 参数:ES6 中的剩余参数语法
- 数组差集:集合论中差集的概念及实现
源码实现
js
var differenceWith = baseRest(function (array, values) {
var comparator = last(values);
if (isArrayLikeObject(comparator)) {
comparator = undefined;
}
return isArrayLikeObject(array)
? baseDifference(
array,
baseFlatten(values, 1, isArrayLikeObject, true),
undefined,
comparator
)
: [];
});
实现思路
differenceWith
函数的实现思路非常直接:
- 使用
baseRest
将函数转换为支持剩余参数的形式,这使得函数可以接收任意数量的数组作为参数 - 检查最后一个参数是否为比较器函数,如果最后一个参数也是一个数组,则不将其视为比较器
- 验证第一个参数是否为类数组对象
- 调用
baseDifference
来执行实际的差集计算,使用baseFlatten
将除第一个数组之外的所有数组扁平化 - 将比较器函数传递给
baseDifference
以支持自定义元素比较逻辑
源码解析
1. 函数包装与参数处理
js
var differenceWith = baseRest(function (array, values) {
// 函数体
});
differenceWith
函数首先使用 baseRest
进行包装,这是 Lodash 处理可变参数的标准模式。这种包装带来了几个好处:
- 允许函数接收任意数量的参数,增强了 API 灵活性
- 将传统的参数列表转换为更现代的 Rest 参数形式
- 使得
array
接收第一个参数,而values
以数组形式接收剩余所有参数
这种处理方式使以下调用形式都变得可行:
js
// 基本用法:一个比较数组和一个比较器
_.differenceWith(objects, [{ x: 1, y: 2 }], _.isEqual);
// 多个比较数组和一个比较器
_.differenceWith(objects, [{ x: 1, y: 2 }], [{ x: 3, y: 4 }], _.isEqual);
2. 比较器提取与验证
js
var comparator = last(values);
if (isArrayLikeObject(comparator)) {
comparator = undefined;
}
这段代码的核心任务是从参数中提取出可能的比较器函数。它采用了一种智能的推断方式:
- 使用
last
函数获取values
数组中的最后一个元素 - 检查这个元素是否为类数组对象(通过
isArrayLikeObject
函数) - 如果最后一个参数是类数组对象而不是函数,则将比较器设为
undefined
这种设计考虑了以下使用场景:
js
// 最后一个参数是函数,被视为比较器
_.differenceWith([1, 2], [2, 3], myComparator);
// 最后一个参数是数组,不被视为比较器,而是另一个要排除的值集合
_.differenceWith([1, 2], [2], [3]);
这种自动推断极大地提高了 API 的易用性,用户不需要特别区分比较器参数的位置。
3. 输入类型检查
js
return isArrayLikeObject(array)
? baseDifference(
array,
baseFlatten(values, 1, isArrayLikeObject, true),
undefined,
comparator
)
: [];
在执行实际的差集计算前,函数首先确保第一个参数 array
是一个有效的类数组对象。这是一个重要的防御性编程措施:
- 如果
array
不是类数组对象,计算差集没有意义 - 返回空数组是一个安全且符合预期的行为
- 这种检查避免了在非数组输入上执行操作时可能出现的错误
例如,以下调用会安全地处理不正确的输入:
js
_.differenceWith(null, [1, 2], _.isEqual); // => []
_.differenceWith(42, [1, 2], _.isEqual); // => []
_.differenceWith({}, [1, 2], _.isEqual); // => []
4. 参数扁平化与预处理
js
baseFlatten(values, 1, isArrayLikeObject, true);
baseFlatten
函数在这里扮演着关键角色,它将 values
数组(包含所有要排除的值集合和可能的比较器)进行扁平化处理。参数解析:
values
:要扁平化的数组1
:扁平化深度,只扁平化一层isArrayLikeObject
:用于检测元素是否应该被扁平化的谓词函数true
:指示是否应该移除不符合谓词检查的元素(如比较器函数)
这种扁平化处理使用户可以以多种方式传递要排除的值:
js
// 以下两种调用方式产生相同的结果
_.differenceWith(objects, [{ x: 1, y: 2 }], [{ x: 3, y: 4 }], _.isEqual);
_.differenceWith(objects, [[{ x: 1, y: 2 }], [{ x: 3, y: 4 }]], _.isEqual);
5. 比较器参数位置
js
baseDifference(
array,
baseFlatten(values, 1, isArrayLikeObject, true),
undefined,
comparator
);
在调用 baseDifference
时,比较器被放在第四个参数位置,而第三个参数(通常用于迭代器)设为 undefined
。这与 differenceBy
的调用模式形成对比:
differenceBy
使用第三个参数传递迭代器,第四个参数未使用differenceWith
使用第四个参数传递比较器,第三个参数未使用
这种明确的参数位置区分了两个函数的不同功能:
js
// differenceBy 使用迭代器转换元素后再比较
baseDifference(array, values, iteratee, undefined);
// differenceWith 使用自定义比较器直接比较元素
baseDifference(array, values, undefined, comparator);
6. 差集计算核心逻辑
当所有参数准备就绪后,实际的差集计算由 baseDifference
函数执行,其核心逻辑包括:
- 遍历第一个数组的每个元素
- 当提供了比较器时,使用
arrayIncludesWith
函数代替默认的arrayIncludes
- 对于每个元素,检查它是否存在于扁平化后的排除值数组中
- 如果元素不存在于排除值数组中(根据比较器的判断),则将其添加到结果数组
比较器函数接收两个参数并返回一个布尔值:
js
function comparator(arrVal, othVal) {
// 返回 true 表示两个值相等,第一个数组中的元素将被排除
// 返回 false 表示两个值不相等,元素将被保留
}
当比较器返回 true
时,表示找到了匹配项,当前元素将被排除;否则,元素将被保留在结果数组中。
总结
differenceWith
函数通过引入自定义比较器,大大增强了 Lodash 中差集操作的灵活性和强大性。它的实现遵循了函数式编程的思想,通过组合多个核心函数(baseRest
、last
、baseDifference
、baseFlatten
等)来实现复杂的功能。