Lodash 源码阅读-intersectionBy
概述
intersectionBy 函数用于计算多个数组的交集,并支持通过迭代器函数对数组元素进行转换后再比较。该函数返回一个新数组,其中包含来自第一个数组的元素,这些元素在经过迭代器函数处理后,存在于所有传入的数组中。函数使用 SameValueZero 算法进行值的比较,保持返回元素的顺序与第一个数组中相同。
前置学习
依赖函数
- baseRest:模拟 ES6 rest 参数功能的工具函数,将不定数量的参数收集到一个数组中
- last:获取数组的最后一个元素,实现为
array[array.length - 1] - arrayMap:内部版本的 Array.map,对数组中的每个元素应用转换函数并返回新数组
- castArrayLikeObject:将输入值转换为数组或类数组对象,对于非类数组对象则返回空数组
- baseIntersection:核心实现函数,计算多个数组的交集,支持自定义迭代器和比较器
- getIteratee:获取适当的迭代器函数,支持多种形式(函数、字符串路径、对象等)
技术知识
- 函数柯里化与参数收集:通过
baseRest实现的参数处理技术 - 高阶函数:函数作为参数和返回值的使用模式
- 类数组对象处理:处理类似数组但不是真正数组的对象(如 arguments、DOM 列表等)
- SameValueZero 比较算法:JavaScript 中的相等性比较算法,类似
===但将+0和-0视为相等 - 柯里化和函数组合:通过函数组合构建复杂功能的函数式编程技术
源码实现
var intersectionBy = baseRest(function (arrays) {
var iteratee = last(arrays),
mapped = arrayMap(arrays, castArrayLikeObject);
if (iteratee === last(mapped)) {
iteratee = undefined;
} else {
mapped.pop();
}
return mapped.length && mapped[0] === arrays[0]
? baseIntersection(mapped, getIteratee(iteratee, 2))
: [];
});
实现思路
intersectionBy 函数采用了函数式编程的组合方式,通过以下步骤实现其功能:
- 使用
baseRest处理不定数量的参数,将所有参数收集到arrays数组中 - 假设最后一个参数可能是迭代器函数,先将其提取备用
- 使用
arrayMap和castArrayLikeObject确保所有输入都是有效的数组,非数组类型转为空数组 - 通过检查转换前后的最后一个参数是否相同,判断其是否为有效的迭代器函数
- 验证参数的有效性后,调用
baseIntersection计算交集,同时传入处理好的迭代器函数 - 通过处理边缘情况(无效输入、空数组等)确保函数行为的稳定性和可预测性
这种设计使函数既能处理多种输入形式,又能保持实现的简洁性和健壮性。
源码解析
1. 函数定义与参数收集
var intersectionBy = baseRest(function (arrays) {
这行代码使用 baseRest 函数包装了主体函数,实现了类似 ES6 剩余参数的功能。当调用 intersectionBy 时,所有传入的参数都被收集到 arrays 数组中:
// 调用方式
_.intersectionBy([1.2, 2.4], [2.5, 3.6], Math.floor);
// arrays = [[1.2, 2.4], [2.5, 3.6], Math.floor]
baseRest 确保了包装后的函数保留原始函数的名称和参数处理能力。
2. 迭代器提取与参数处理
var iteratee = last(arrays),
mapped = arrayMap(arrays, castArrayLikeObject);
这两行代码完成了两个关键操作:
- 获取
arrays中的最后一个元素,假设它可能是迭代器函数 - 对
arrays中的每个元素应用castArrayLikeObject函数,确保它们都是有效的数组
last 函数简单地返回数组的最后一个元素,arrayMap 遍历数组并对每个元素应用指定的函数,而 castArrayLikeObject 确保输入是数组或类数组对象,非类数组对象则返回空数组。
通过这些处理,mapped 数组包含了所有转换后的参数:
// 原始参数
arrays = [[1.2, 2.4], [2.5, 3.6], Math.floor];
// 转换后
mapped = [[1.2, 2.4], [2.5, 3.6], []]; // Math.floor 不是数组,变成了 []
3. 迭代器验证
if (iteratee === last(mapped)) {
iteratee = undefined;
} else {
mapped.pop();
}
这段代码检查最后一个参数是否真的是迭代器函数:
- 如果
iteratee === last(mapped),说明最后一个参数是数组(因为转换前后相同),不是迭代器函数,将iteratee设为undefined - 否则,说明最后一个参数是迭代器函数(转换为空数组后已不同),需要从
mapped中移除这个空数组
这种智能判断使 intersectionBy 能够在不需要额外标记的情况下,正确区分数组参数和迭代器函数参数。
边缘情况处理:
// 没有提供迭代器的情况
_.intersectionBy([1, 2], [2, 3]);
// iteratee = [2, 3], last(mapped) = [2, 3]
// 由于相等,iteratee 被设为 undefined
// 提供迭代器的情况
_.intersectionBy([1, 2], [2, 3], Math.floor);
// iteratee = Math.floor, last(mapped) = []
// 不相等,调用 mapped.pop() 移除空数组
4. 条件执行与交集计算
return mapped.length && mapped[0] === arrays[0]
? baseIntersection(mapped, getIteratee(iteratee, 2))
: [];
这行代码首先进行有效性验证,然后决定是否执行交集计算:
-
有效性验证:
mapped.length:确保至少有一个有效数组mapped[0] === arrays[0]:确保第一个数组是有效的类数组对象
-
条件执行:
- 如果验证通过:调用
baseIntersection计算交集 - 如果验证失败:直接返回空数组
[]
- 如果验证通过:调用
-
迭代器处理:
- 通过
getIteratee(iteratee, 2)获取适当的迭代器函数 - 参数
2表示迭代器将接收两个参数(值和索引)
- 通过
getIteratee 函数根据不同类型的输入返回合适的迭代器函数,支持函数、字符串路径、对象等多种形式,使 intersectionBy 能够灵活处理各种迭代需求:
// 函数形式
_.intersectionBy([2.1, 1.2], [2.3, 3.4], Math.floor);
// 字符串形式(属性路径)
_.intersectionBy([{ x: 1 }, { x: 2 }], [{ x: 2 }, { x: 1 }], "x");
// 无迭代器(使用默认恒等函数)
_.intersectionBy([1, 2], [2, 3]);
5. 交集计算核心 - baseIntersection
baseIntersection 是实际执行交集运算的核心函数,它接受数组集合和可选的迭代器函数。其主要工作原理是:
- 以第一个数组作为基准,遍历其中的每个元素
- 对于每个元素,检查它(或经过迭代器处理后的值)是否在所有其他数组中都存在
- 如果元素在所有数组中都存在,将其添加到结果数组中
- 返回找到的所有共同元素
在实际实现中,baseIntersection 还包含了性能优化策略:
- 对于大型数组(长度大于 120),使用
SetCache缓存来加速查找 - 优先处理较小的数组,减少比较次数
- 支持自定义比较器,增强灵活性
总结
-
参数处理与核心逻辑分离:将参数验证和处理与核心计算逻辑分开,使代码更清晰、更易于维护,也方便在不同函数间复用核心逻辑。
-
灵活的接口设计:支持多种参数形式(函数、字符串路径等)作为迭代器,增强了 API 的易用性和表达能力,减少用户的学习成本。
-
预处理策略:先对输入数据进行标准化处理再执行主要逻辑,这种方式有效减少边界情况的处理复杂度,提高代码稳定性。
现代 JavaScript 中可通过 Array.filter() 与 Set 结合实现类似功能:(arr1, arr2, iteratee) => arr1.filter(x => arr2.some(y => iteratee(x) === iteratee(y))),写法更简洁但在多数组和性能优化方面不如 Lodash;而 ES6 的 rest 参数可直接替代 baseRest 的包装方式。