Lodash 源码阅读-intersection
概述
intersection 函数用于计算多个数组的交集,返回一个新数组,其中包含所有传入数组中共同存在的元素。该函数使用 SameValueZero 算法进行相等性比较(类似于 === 但将 +0 和 -0 视为相等)。返回数组中元素的顺序和引用保持与第一个数组中相同。
前置学习
依赖函数
- baseRest:一个用于实现类似 ES6 rest 参数功能的工具函数,处理不定数量的参数
- arrayMap:内部工具函数,用于对数组中的每个元素应用转换函数
- castArrayLikeObject:将输入值转换为类数组对象,如果不是类数组对象则返回空数组
- baseIntersection:核心函数,实现多个数组交集的计算,支持迭代器和自定义比较器
技术知识
- SameValueZero 比较算法:JavaScript 中的特殊比较算法,与严格相等(
===)类似,但特殊处理+0和-0 - 不定参数处理:处理任意数量函数参数的编程技术
- 类数组对象:拥有 length 属性且可以通过数字索引访问元素的对象(如数组、arguments、DOM 列表等)
- 高阶函数:接受函数作为参数或返回函数的函数
源码实现
var intersection = baseRest(function (arrays) {
var mapped = arrayMap(arrays, castArrayLikeObject);
return mapped.length && mapped[0] === arrays[0]
? baseIntersection(mapped)
: [];
});
实现思路
intersection 函数采用了简洁优雅的实现方式:
- 使用
baseRest将函数包装成能处理不定数量参数的形式,所有传入的参数被收集到arrays数组中 - 通过
arrayMap配合castArrayLikeObject处理每个输入参数,确保它们都是有效的数组或类数组对象 - 进行有效性检查:确保至少有一个有效输入,且第一个输入是有效的类数组对象
- 如果检查通过,调用
baseIntersection计算所有数组的交集;否则返回空数组
这种设计既能高效处理正常情况,又能优雅地处理各种边缘情况,如空输入、非数组输入等。
源码解析
1. 函数定义与参数收集
var intersection = baseRest(function(arrays) {
这行代码使用 baseRest 包装了一个函数,使其能接收任意数量的参数。当调用 intersection 时,所有参数都会被收集到 arrays 数组中:
// 调用方式
_.intersection([1, 2], [2, 3], [2, 4]);
// arrays = [[1, 2], [2, 3], [2, 4]]
baseRest 实际上是模拟了 ES6 中的剩余参数(rest parameters)功能,让 intersection 可以接受不确定数量的数组参数。
2. 输入参数处理
var mapped = arrayMap(arrays, castArrayLikeObject);
这一步对收集到的所有参数进行预处理:
arrayMap遍历arrays中的每个元素- 对每个元素应用
castArrayLikeObject函数 - 返回一个新数组
mapped,包含所有处理后的结果
castArrayLikeObject 函数会判断输入是否为类数组对象:
- 如果是,直接返回该值
- 如果不是,返回空数组
[]
这样处理可以确保:
- 函数能够处理真正的数组
- 函数能够处理类数组对象(如
arguments、DOM 集合等) - 对于无效输入,转换为空数组,便于后续统一处理
例如:
// 有效输入处理
mapped = [
[1, 2],
[2, 3],
[2, 4],
]; // 原始数组保持不变
// 包含无效输入时
_.intersection([1, 2], null, "abc");
// arrays = [[1, 2], null, "abc"]
// mapped = [[1, 2], [], []] // 无效输入被转换为空数组
3. 有效性检查与交集计算
return mapped.length && mapped[0] === arrays[0] ? baseIntersection(mapped) : [];
这行代码包含两个关键逻辑:
-
有效性检查:通过
mapped.length && mapped[0] === arrays[0]验证:mapped.length:确保至少有一个输入mapped[0] === arrays[0]:确保第一个输入是有效的类数组对象(如果第一个输入就无效,则mapped[0]会是空数组而非arrays[0])
-
条件执行:
- 如果检查通过:调用
baseIntersection(mapped)计算所有数组的交集 - 如果检查失败:直接返回空数组
[]
- 如果检查通过:调用
这种设计优雅地处理了各种边缘情况:
// 正常情况
_.intersection([1, 2, 3], [2, 3, 4]); // [2, 3]
// 第一个参数无效
_.intersection({}, [1, 2]); // [] (因为 mapped[0] !== arrays[0])
// 没有共同元素
_.intersection([1, 2], [3, 4]); // [] (由 baseIntersection 返回)
4. 核心交集计算 - baseIntersection
baseIntersection 是实际执行交集运算的核心函数。虽然源码中没有展示其实现,但我们可以理解它的基本工作原理:
- 以第一个数组作为基准,遍历其中的每个元素
- 对于每个元素,检查它是否在所有其他数组中都存在
- 如果元素在所有数组中都存在,将其添加到结果数组中
在实际实现中,baseIntersection 还包含了性能优化,如:
- 优先使用较小的数组作为基准数组,减少比较次数
- 使用缓存避免重复比较
- 支持自定义比较器和迭代器函数
这种实现确保了交集计算的正确性和高效性,同时保持了与第一个数组相同的元素顺序和引用。
总结
从 intersection 函数实现中,我们可以提炼出三个实用的编程技巧:
-
灵活参数处理 - 使用类似
baseRest的方式处理不定参数,让函数接口更友好。在自己设计工具库时,可考虑支持多种参数形式而非固定格式。 -
分层验证逻辑 - 先验证输入有效性,再进行核心计算。这种"验证前置"的模式可以让代码逻辑更清晰,也更容易维护。
现代 JavaScript 中,我们也可以用 Set 配合数组方法实现类似功能,在处理大型数据集时可能性能更好,但 intersection 的实现在兼容性和特殊值处理上更为健壮。