Lodash 源码阅读-intersectionWith
概述
intersectionWith 函数用于计算多个数组的交集,并支持自定义比较器。它返回一个新数组,包含所有传入数组中共同存在的元素(基于比较器的判断)。与基础版本的 intersection 不同,它允许我们自定义元素相等的判断逻辑,特别适合处理复杂对象数组的交集计算。
前置学习
依赖函数
- baseRest:一个用于实现类似 ES6 rest 参数功能的工具函数,处理不定数量的参数
- last:获取数组最后一个元素的辅助函数,用于提取可能的比较器函数
- arrayMap:内部工具函数,用于对数组中的每个元素应用转换函数
- castArrayLikeObject:将输入值转换为类数组对象,如果不是类数组对象则返回空数组
- baseIntersection:核心函数,实现多个数组交集的计算,支持迭代器和自定义比较器
技术知识
- 函数式编程:高阶函数、柯里化等概念,了解如何使用和传递函数作为参数
- 数组操作:数组交集、映射等基础操作的实现方法
- 类型判断:JavaScript 中如何判断函数类型、检测类数组对象等
- 自定义比较器:了解如何设计和使用自定义比较逻辑
源码实现
var intersectionWith = baseRest(function (arrays) {
var comparator = last(arrays),
mapped = arrayMap(arrays, castArrayLikeObject);
comparator = typeof comparator == "function" ? comparator : undefined;
if (comparator) {
mapped.pop();
}
return mapped.length && mapped[0] === arrays[0]
? baseIntersection(mapped, undefined, comparator)
: [];
});
实现思路
intersectionWith 函数的实现思路非常直接:
- 使用
baseRest收集所有传入的参数到一个数组中 - 检查最后一个参数是否为函数,如果是则作为比较器使用
- 将所有输入参数转换为标准数组格式,确保处理一致性
- 如果比较器存在,从转换后的数组集合中移除它(因为它不是要进行交集操作的数组)
- 进行有效性检查,确保有数据可处理且转换成功
- 调用
baseIntersection核心函数计算交集,传入比较器函数
这个实现既能高效处理正常情况,也能应对各种边缘情况,如无效输入、空数组等。
源码解析
1. 函数定义与参数预处理
var intersectionWith = baseRest(function(arrays) {
var comparator = last(arrays),
mapped = arrayMap(arrays, castArrayLikeObject);
这段代码完成了两项关键任务:
-
收集参数:
baseRest将所有传入参数收集到arrays数组中。例如,调用_.intersectionWith(arr1, arr2, customCompare)后,arrays会等于[arr1, arr2, customCompare]。 -
参数预处理:
comparator = last(arrays)获取最后一个参数,假设它可能是比较器函数mapped = arrayMap(arrays, castArrayLikeObject)将每个参数转换为有效的数组格式
castArrayLikeObject 函数确保所有输入都转换为可用的数组格式:
function castArrayLikeObject(value) {
return isArrayLikeObject(value) ? value : [];
}
这一步处理使函数能够接受真实数组、类数组对象,同时将无效输入统一转换为空数组,为后续处理提供一致的数据格式。
2. 比较器处理
comparator = typeof comparator == "function" ? comparator : undefined;
if (comparator) {
mapped.pop();
}
这段代码负责确认和处理比较器函数:
-
类型检查:验证最后一个参数是否真的是函数,如果是则保留它作为比较器;如果不是,则设置为
undefined。 -
移除比较器:如果确实有比较器,则从
mapped数组中移除最后一个元素。这是因为比较器只是用来指导如何比较元素,它本身不应该参与交集计算。
这一设计允许用户选择性地提供比较器:如果不提供,函数将使用默认的相等性比较;如果提供了,则使用自定义比较逻辑。
3. 交集计算
return mapped.length && mapped[0] === arrays[0]
? baseIntersection(mapped, undefined, comparator)
: [];
这段代码进行了三个关键操作:
-
有效性检查:通过
mapped.length && mapped[0] === arrays[0]确保:- 至少有一个有效输入
- 第一个输入是有效的类数组对象(如果第一个输入无效,转换后
mapped[0]会是一个空数组,而非原始的arrays[0])
-
交集计算:如果检查通过,调用
baseIntersection执行实际的交集计算,传入:- 转换后的数组集合
mapped - 迭代器参数(这里是
undefined,表示不需要迭代器) - 自定义比较器函数
comparator
- 转换后的数组集合
-
边界处理:如果检查失败,直接返回空数组
[]
baseIntersection 函数实现了核心的交集计算逻辑,它会遍历第一个数组中的每个元素,检查该元素是否在所有其他数组中都存在(使用提供的比较器)。
4. 使用示例分析
为了更直观地理解这个函数的工作原理,我们来分析一个实际使用示例:
// 定义两个对象数组
var objects = [
{ x: 1, y: 2 },
{ x: 2, y: 1 },
];
var others = [
{ x: 1, y: 1 },
{ x: 1, y: 2 },
];
// 使用自定义比较器计算交集
_.intersectionWith(objects, others, _.isEqual);
// => [{ 'x': 1, 'y': 2 }]
执行流程:
arrays = [objects, others, _.isEqual],包含两个数组和一个比较器函数comparator = _.isEqual,最后一个参数确实是函数mapped = [objects, others, ???](第三项在下一步会被移除)- 确认比较器是函数后,执行
mapped.pop(),现在mapped = [objects, others] - 有效性检查通过,调用
baseIntersection(mapped, undefined, _.isEqual) baseIntersection使用_.isEqual比较两个数组中的对象,找到满足条件的交集元素{ 'x': 1, 'y': 2 }
比较器 _.isEqual 允许深度比较对象的所有属性,而不仅仅是检查对象引用是否相同,这使得我们能够找到两个数组中结构相同的对象。
总结
从 intersectionWith 函数的实现中,我们可以学到几个实用的编程技巧:
-
自定义比较逻辑:通过将比较器作为参数传递,可以灵活处理复杂数据结构的相等性判断,比简单的全等比较更强大。
-
健壮的参数处理:函数内部对各种边界情况(非数组输入、无效比较器等)进行了处理,确保即使在错误输入下也能给出合理结果而不崩溃。
-
函数式与命令式结合:整体采用函数式风格组织代码,但在必要时也使用命令式代码(如条件判断、数组操作)提高实现简洁性。
在现代 JavaScript 中,我们可以使用 Array.filter 和 Set 等原生功能实现类似功能,但处理复杂对象比较时仍需自定义逻辑。Lodash 的实现在边界情况处理和兼容性方面更为全面。