Lodash源码阅读-intersection

226 阅读5分钟

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 函数采用了简洁优雅的实现方式:

  1. 使用 baseRest 将函数包装成能处理不定数量参数的形式,所有传入的参数被收集到 arrays 数组中
  2. 通过 arrayMap 配合 castArrayLikeObject 处理每个输入参数,确保它们都是有效的数组或类数组对象
  3. 进行有效性检查:确保至少有一个有效输入,且第一个输入是有效的类数组对象
  4. 如果检查通过,调用 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 函数会判断输入是否为类数组对象:

  • 如果是,直接返回该值
  • 如果不是,返回空数组 []

这样处理可以确保:

  1. 函数能够处理真正的数组
  2. 函数能够处理类数组对象(如 arguments、DOM 集合等)
  3. 对于无效输入,转换为空数组,便于后续统一处理

例如:

// 有效输入处理
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) : [];

这行代码包含两个关键逻辑:

  1. 有效性检查:通过 mapped.length && mapped[0] === arrays[0] 验证:

    • mapped.length:确保至少有一个输入
    • mapped[0] === arrays[0]:确保第一个输入是有效的类数组对象(如果第一个输入就无效,则 mapped[0] 会是空数组而非 arrays[0]
  2. 条件执行

    • 如果检查通过:调用 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 是实际执行交集运算的核心函数。虽然源码中没有展示其实现,但我们可以理解它的基本工作原理:

  1. 以第一个数组作为基准,遍历其中的每个元素
  2. 对于每个元素,检查它是否在所有其他数组中都存在
  3. 如果元素在所有数组中都存在,将其添加到结果数组中

在实际实现中,baseIntersection 还包含了性能优化,如:

  • 优先使用较小的数组作为基准数组,减少比较次数
  • 使用缓存避免重复比较
  • 支持自定义比较器和迭代器函数

这种实现确保了交集计算的正确性和高效性,同时保持了与第一个数组相同的元素顺序和引用。

总结

intersection 函数实现中,我们可以提炼出三个实用的编程技巧:

  1. 灵活参数处理 - 使用类似 baseRest 的方式处理不定参数,让函数接口更友好。在自己设计工具库时,可考虑支持多种参数形式而非固定格式。

  2. 分层验证逻辑 - 先验证输入有效性,再进行核心计算。这种"验证前置"的模式可以让代码逻辑更清晰,也更容易维护。

现代 JavaScript 中,我们也可以用 Set 配合数组方法实现类似功能,在处理大型数据集时可能性能更好,但 intersection 的实现在兼容性和特殊值处理上更为健壮。