Lodash源码阅读-intersectionWith

110 阅读6分钟

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 函数的实现思路非常直接:

  1. 使用 baseRest 收集所有传入的参数到一个数组中
  2. 检查最后一个参数是否为函数,如果是则作为比较器使用
  3. 将所有输入参数转换为标准数组格式,确保处理一致性
  4. 如果比较器存在,从转换后的数组集合中移除它(因为它不是要进行交集操作的数组)
  5. 进行有效性检查,确保有数据可处理且转换成功
  6. 调用 baseIntersection 核心函数计算交集,传入比较器函数

这个实现既能高效处理正常情况,也能应对各种边缘情况,如无效输入、空数组等。

源码解析

1. 函数定义与参数预处理

var intersectionWith = baseRest(function(arrays) {
  var comparator = last(arrays),
      mapped = arrayMap(arrays, castArrayLikeObject);

这段代码完成了两项关键任务:

  1. 收集参数: baseRest 将所有传入参数收集到 arrays 数组中。例如,调用 _.intersectionWith(arr1, arr2, customCompare) 后,arrays 会等于 [arr1, arr2, customCompare]

  2. 参数预处理:

    • 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();
}

这段代码负责确认和处理比较器函数:

  1. 类型检查:验证最后一个参数是否真的是函数,如果是则保留它作为比较器;如果不是,则设置为 undefined

  2. 移除比较器:如果确实有比较器,则从 mapped 数组中移除最后一个元素。这是因为比较器只是用来指导如何比较元素,它本身不应该参与交集计算。

这一设计允许用户选择性地提供比较器:如果不提供,函数将使用默认的相等性比较;如果提供了,则使用自定义比较逻辑。

3. 交集计算

return mapped.length && mapped[0] === arrays[0]
  ? baseIntersection(mapped, undefined, comparator)
  : [];

这段代码进行了三个关键操作:

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

    • 至少有一个有效输入
    • 第一个输入是有效的类数组对象(如果第一个输入无效,转换后 mapped[0] 会是一个空数组,而非原始的 arrays[0]
  2. 交集计算:如果检查通过,调用 baseIntersection 执行实际的交集计算,传入:

    • 转换后的数组集合 mapped
    • 迭代器参数(这里是 undefined,表示不需要迭代器)
    • 自定义比较器函数 comparator
  3. 边界处理:如果检查失败,直接返回空数组 []

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 }]

执行流程:

  1. arrays = [objects, others, _.isEqual],包含两个数组和一个比较器函数
  2. comparator = _.isEqual,最后一个参数确实是函数
  3. mapped = [objects, others, ???](第三项在下一步会被移除)
  4. 确认比较器是函数后,执行 mapped.pop(),现在 mapped = [objects, others]
  5. 有效性检查通过,调用 baseIntersection(mapped, undefined, _.isEqual)
  6. baseIntersection 使用 _.isEqual 比较两个数组中的对象,找到满足条件的交集元素 { 'x': 1, 'y': 2 }

比较器 _.isEqual 允许深度比较对象的所有属性,而不仅仅是检查对象引用是否相同,这使得我们能够找到两个数组中结构相同的对象。

总结

intersectionWith 函数的实现中,我们可以学到几个实用的编程技巧:

  1. 自定义比较逻辑:通过将比较器作为参数传递,可以灵活处理复杂数据结构的相等性判断,比简单的全等比较更强大。

  2. 健壮的参数处理:函数内部对各种边界情况(非数组输入、无效比较器等)进行了处理,确保即使在错误输入下也能给出合理结果而不崩溃。

  3. 函数式与命令式结合:整体采用函数式风格组织代码,但在必要时也使用命令式代码(如条件判断、数组操作)提高实现简洁性。

在现代 JavaScript 中,我们可以使用 Array.filterSet 等原生功能实现类似功能,但处理复杂对象比较时仍需自定义逻辑。Lodash 的实现在边界情况处理和兼容性方面更为全面。