Lodash源码阅读-intersectionBy

121 阅读6分钟

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 函数采用了函数式编程的组合方式,通过以下步骤实现其功能:

  1. 使用 baseRest 处理不定数量的参数,将所有参数收集到 arrays 数组中
  2. 假设最后一个参数可能是迭代器函数,先将其提取备用
  3. 使用 arrayMapcastArrayLikeObject 确保所有输入都是有效的数组,非数组类型转为空数组
  4. 通过检查转换前后的最后一个参数是否相同,判断其是否为有效的迭代器函数
  5. 验证参数的有效性后,调用 baseIntersection 计算交集,同时传入处理好的迭代器函数
  6. 通过处理边缘情况(无效输入、空数组等)确保函数行为的稳定性和可预测性

这种设计使函数既能处理多种输入形式,又能保持实现的简洁性和健壮性。

源码解析

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);

这两行代码完成了两个关键操作:

  1. 获取 arrays 中的最后一个元素,假设它可能是迭代器函数
  2. 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))
  : [];

这行代码首先进行有效性验证,然后决定是否执行交集计算:

  1. 有效性验证

    • mapped.length:确保至少有一个有效数组
    • mapped[0] === arrays[0]:确保第一个数组是有效的类数组对象
  2. 条件执行

    • 如果验证通过:调用 baseIntersection 计算交集
    • 如果验证失败:直接返回空数组 []
  3. 迭代器处理

    • 通过 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 是实际执行交集运算的核心函数,它接受数组集合和可选的迭代器函数。其主要工作原理是:

  1. 以第一个数组作为基准,遍历其中的每个元素
  2. 对于每个元素,检查它(或经过迭代器处理后的值)是否在所有其他数组中都存在
  3. 如果元素在所有数组中都存在,将其添加到结果数组中
  4. 返回找到的所有共同元素

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

  • 对于大型数组(长度大于 120),使用 SetCache 缓存来加速查找
  • 优先处理较小的数组,减少比较次数
  • 支持自定义比较器,增强灵活性

总结

  1. 参数处理与核心逻辑分离:将参数验证和处理与核心计算逻辑分开,使代码更清晰、更易于维护,也方便在不同函数间复用核心逻辑。

  2. 灵活的接口设计:支持多种参数形式(函数、字符串路径等)作为迭代器,增强了 API 的易用性和表达能力,减少用户的学习成本。

  3. 预处理策略:先对输入数据进行标准化处理再执行主要逻辑,这种方式有效减少边界情况的处理复杂度,提高代码稳定性。

现代 JavaScript 中可通过 Array.filter() 与 Set 结合实现类似功能:(arr1, arr2, iteratee) => arr1.filter(x => arr2.some(y => iteratee(x) === iteratee(y))),写法更简洁但在多数组和性能优化方面不如 Lodash;而 ES6 的 rest 参数可直接替代 baseRest 的包装方式。