Lodash源码阅读-differenceWith

概述

differenceWith 是 Lodash 中的一个数组方法,它类似于 _.difference,但允许我们提供一个自定义比较器函数来判断元素是否应该被排除。它会返回一个新数组,包含存在于第一个数组中但不存在于其他数组中的元素,使用指定的比较器函数进行元素比较。

前置学习

依赖函数

  • baseRest:将一个函数转换为支持剩余参数的函数
  • last:获取数组中的最后一个元素
  • isArrayLikeObject:检查一个值是否为类数组对象
  • baseDifference:计算数组差集的基础实现
  • baseFlatten:扁平化数组的基础实现

技术知识

  • 高阶函数:函数作为参数传递和返回
  • 类数组对象:JavaScript 中类数组对象的概念和处理
  • 自定义比较器:如何实现和使用自定义比较逻辑
  • Rest 参数:ES6 中的剩余参数语法
  • 数组差集:集合论中差集的概念及实现

源码实现

js 复制代码
var differenceWith = baseRest(function (array, values) {
  var comparator = last(values);
  if (isArrayLikeObject(comparator)) {
    comparator = undefined;
  }
  return isArrayLikeObject(array)
    ? baseDifference(
        array,
        baseFlatten(values, 1, isArrayLikeObject, true),
        undefined,
        comparator
      )
    : [];
});

实现思路

differenceWith 函数的实现思路非常直接:

  1. 使用 baseRest 将函数转换为支持剩余参数的形式,这使得函数可以接收任意数量的数组作为参数
  2. 检查最后一个参数是否为比较器函数,如果最后一个参数也是一个数组,则不将其视为比较器
  3. 验证第一个参数是否为类数组对象
  4. 调用 baseDifference 来执行实际的差集计算,使用 baseFlatten 将除第一个数组之外的所有数组扁平化
  5. 将比较器函数传递给 baseDifference 以支持自定义元素比较逻辑

源码解析

1. 函数包装与参数处理

js 复制代码
var differenceWith = baseRest(function (array, values) {
  // 函数体
});

differenceWith 函数首先使用 baseRest 进行包装,这是 Lodash 处理可变参数的标准模式。这种包装带来了几个好处:

  • 允许函数接收任意数量的参数,增强了 API 灵活性
  • 将传统的参数列表转换为更现代的 Rest 参数形式
  • 使得 array 接收第一个参数,而 values 以数组形式接收剩余所有参数

这种处理方式使以下调用形式都变得可行:

js 复制代码
// 基本用法:一个比较数组和一个比较器
_.differenceWith(objects, [{ x: 1, y: 2 }], _.isEqual);

// 多个比较数组和一个比较器
_.differenceWith(objects, [{ x: 1, y: 2 }], [{ x: 3, y: 4 }], _.isEqual);

2. 比较器提取与验证

js 复制代码
var comparator = last(values);
if (isArrayLikeObject(comparator)) {
  comparator = undefined;
}

这段代码的核心任务是从参数中提取出可能的比较器函数。它采用了一种智能的推断方式:

  • 使用 last 函数获取 values 数组中的最后一个元素
  • 检查这个元素是否为类数组对象(通过 isArrayLikeObject 函数)
  • 如果最后一个参数是类数组对象而不是函数,则将比较器设为 undefined

这种设计考虑了以下使用场景:

js 复制代码
// 最后一个参数是函数,被视为比较器
_.differenceWith([1, 2], [2, 3], myComparator);

// 最后一个参数是数组,不被视为比较器,而是另一个要排除的值集合
_.differenceWith([1, 2], [2], [3]);

这种自动推断极大地提高了 API 的易用性,用户不需要特别区分比较器参数的位置。

3. 输入类型检查

js 复制代码
return isArrayLikeObject(array)
  ? baseDifference(
      array,
      baseFlatten(values, 1, isArrayLikeObject, true),
      undefined,
      comparator
    )
  : [];

在执行实际的差集计算前,函数首先确保第一个参数 array 是一个有效的类数组对象。这是一个重要的防御性编程措施:

  • 如果 array 不是类数组对象,计算差集没有意义
  • 返回空数组是一个安全且符合预期的行为
  • 这种检查避免了在非数组输入上执行操作时可能出现的错误

例如,以下调用会安全地处理不正确的输入:

js 复制代码
_.differenceWith(null, [1, 2], _.isEqual); // => []
_.differenceWith(42, [1, 2], _.isEqual); // => []
_.differenceWith({}, [1, 2], _.isEqual); // => []

4. 参数扁平化与预处理

js 复制代码
baseFlatten(values, 1, isArrayLikeObject, true);

baseFlatten 函数在这里扮演着关键角色,它将 values 数组(包含所有要排除的值集合和可能的比较器)进行扁平化处理。参数解析:

  • values:要扁平化的数组
  • 1:扁平化深度,只扁平化一层
  • isArrayLikeObject:用于检测元素是否应该被扁平化的谓词函数
  • true:指示是否应该移除不符合谓词检查的元素(如比较器函数)

这种扁平化处理使用户可以以多种方式传递要排除的值:

js 复制代码
// 以下两种调用方式产生相同的结果
_.differenceWith(objects, [{ x: 1, y: 2 }], [{ x: 3, y: 4 }], _.isEqual);
_.differenceWith(objects, [[{ x: 1, y: 2 }], [{ x: 3, y: 4 }]], _.isEqual);

5. 比较器参数位置

js 复制代码
baseDifference(
  array,
  baseFlatten(values, 1, isArrayLikeObject, true),
  undefined,
  comparator
);

在调用 baseDifference 时,比较器被放在第四个参数位置,而第三个参数(通常用于迭代器)设为 undefined。这与 differenceBy 的调用模式形成对比:

  • differenceBy 使用第三个参数传递迭代器,第四个参数未使用
  • differenceWith 使用第四个参数传递比较器,第三个参数未使用

这种明确的参数位置区分了两个函数的不同功能:

js 复制代码
// differenceBy 使用迭代器转换元素后再比较
baseDifference(array, values, iteratee, undefined);

// differenceWith 使用自定义比较器直接比较元素
baseDifference(array, values, undefined, comparator);

6. 差集计算核心逻辑

当所有参数准备就绪后,实际的差集计算由 baseDifference 函数执行,其核心逻辑包括:

  • 遍历第一个数组的每个元素
  • 当提供了比较器时,使用 arrayIncludesWith 函数代替默认的 arrayIncludes
  • 对于每个元素,检查它是否存在于扁平化后的排除值数组中
  • 如果元素不存在于排除值数组中(根据比较器的判断),则将其添加到结果数组

比较器函数接收两个参数并返回一个布尔值:

js 复制代码
function comparator(arrVal, othVal) {
  // 返回 true 表示两个值相等,第一个数组中的元素将被排除
  // 返回 false 表示两个值不相等,元素将被保留
}

当比较器返回 true 时,表示找到了匹配项,当前元素将被排除;否则,元素将被保留在结果数组中。

总结

differenceWith 函数通过引入自定义比较器,大大增强了 Lodash 中差集操作的灵活性和强大性。它的实现遵循了函数式编程的思想,通过组合多个核心函数(baseRestlastbaseDifferencebaseFlatten 等)来实现复杂的功能。

相关推荐
JarvanMo8 分钟前
借助FlutterFire CLI实现Flutter与Firebase的多环境配置
前端·flutter
Jedi Hongbin22 分钟前
echarts自定义图表--仪表盘
前端·javascript·echarts
凯哥197027 分钟前
Sciter.js指南 - 桌面GUI开发时使用第三方模块
前端
边洛洛27 分钟前
对Electron打包的exe文件进行反解析
前端·javascript·electron
财神爷亲闺女27 分钟前
js 实现pc端鼠标横向拖动滚动
前端
用户20311966009627 分钟前
sheet在SwiftUI中的基本用法
前端
晴殇i28 分钟前
一行代码搞定防抖节流:JavaScript新特性解析
前端·javascript
David凉宸31 分钟前
HTML表单(二)
前端