Lodash 源码阅读-differenceBy
概述
differenceBy
是 Lodash 中的一个数组操作函数,它用于计算第一个数组与其他数组的差集,并且支持通过迭代器函数指定比较的条件。它与普通的 difference
函数的区别在于,它可以对每个元素应用转换函数后再进行比较,这使得它在处理复杂对象或需要特定比较条件的场景下特别有用。
前置学习
依赖函数
- baseRest:处理剩余参数的基础函数,将参数转换为符合特定 API 要求的形式
- last:获取数组的最后一个元素
- isArrayLikeObject:检查值是否是类数组对象
- baseDifference:计算差集的内部实现函数
- baseFlatten:将嵌套数组扁平化的内部函数
- getIteratee:获取迭代器函数的工具函数
技术知识
- 函数式编程:柯里化、函数组合等概念
- 参数处理技巧:处理可变参数和参数重载
- 类型检查:JavaScript 中的类型检查方法
- 数组操作:数组扁平化、差集计算等操作
- 迭代器模式:如何使用迭代器转换数据
源码实现
js
var differenceBy = baseRest(function (array, values) {
var iteratee = last(values);
if (isArrayLikeObject(iteratee)) {
iteratee = undefined;
}
return isArrayLikeObject(array)
? baseDifference(
array,
baseFlatten(values, 1, isArrayLikeObject, true),
getIteratee(iteratee, 2)
)
: [];
});
实现思路
differenceBy
函数的实现非常简洁优雅,主要分为以下几个步骤:
- 首先,通过
baseRest
包装原始函数,处理函数的可变参数 - 从参数中提取最后一个值作为潜在的迭代器函数
- 检查这个值是否为数组或类数组对象,如果是,则不将其视为迭代器函数
- 检查第一个参数(要处理的数组)是否为数组或类数组对象
- 如果是数组,则使用
baseDifference
计算差集,否则返回空数组 - 在计算差集时,将除第一个参数外的所有参数扁平化为一个数组
- 根据提供的迭代器函数(或默认迭代器)对比较元素进行转换后再计算差集
这种实现方式既保证了函数的灵活性(支持多种参数形式),又确保了函数的健壮性(各种边缘情况处理)。
源码解析
1. 函数包装处理
js
var differenceBy = baseRest(function (array, values) {
// 函数体
});
baseRest
函数在这里起到了关键作用,它将传统的函数参数列表转换为现代的 Rest 参数形式。具体来说:
array
参数接收第一个传入的参数(要处理的数组)values
参数以数组形式接收剩余的所有参数(要排除的值和可能的迭代器)
这种处理使得 differenceBy
可以接受任意数量的参数,增强了函数的灵活性。例如:
js
// 所有这些调用形式都是有效的
_.differenceBy([2.1, 1.2], [2.3, 3.4], Math.floor);
_.differenceBy([2.1, 1.2], [2.3], [3.4], Math.floor);
_.differenceBy([{ x: 2 }, { x: 1 }], [{ x: 1 }], "x");
2. 迭代器提取
js
var iteratee = last(values);
if (isArrayLikeObject(iteratee)) {
iteratee = undefined;
}
这段代码从 values
数组中提取最后一个元素,作为潜在的迭代器函数。但有一个重要的检查:如果这个元素本身是数组或类数组对象,则不将其视为迭代器函数。
这是一个巧妙的设计,考虑以下情况:
- 如果调用
_.differenceBy([1, 2], [2, 3], Math.floor)
,Math.floor
被提取为迭代器 - 如果调用
_.differenceBy([1, 2], [2, 3], [4])
,最后一个参数[4]
是数组,所以不会被视为迭代器,而是被视为要排除的值之一
这种设计使得 API 更加直观,用户不需要特别区分迭代器参数的位置。
3. 数组类型检查
js
return isArrayLikeObject(array)
? baseDifference(
array,
baseFlatten(values, 1, isArrayLikeObject, true),
getIteratee(iteratee, 2)
)
: [];
这里先检查第一个参数 array
是否为数组或类数组对象。这是必要的安全检查,因为:
- 如果
array
不是数组类型,计算差集没有意义 - 返回空数组是一个安全的默认值,符合函数式编程的理念
例如,以下调用会直接返回空数组:
js
_.differenceBy(null, [1, 2, 3], Math.floor); // => []
_.differenceBy("string", [1, 2, 3], Math.floor); // => []
4. 参数扁平化处理
js
baseFlatten(values, 1, isArrayLikeObject, true);
baseFlatten
函数将 values
数组(包含所有排除值和可能的迭代器)扁平化,其参数含义:
values
:要扁平化的数组1
:扁平化深度,这里只扁平化一层isArrayLikeObject
:用于检查元素是否应该被扁平化的函数true
:是否移除数组中的非数组元素(迭代器函数)
这个扁平化处理允许用户以多种方式传递排除值:
js
// 以下调用是等效的
_.differenceBy([1, 2], [2], [3], Math.floor);
_.differenceBy([1, 2], [[2], [3]], Math.floor);
5. 迭代器处理
js
getIteratee(iteratee, 2);
getIteratee
函数将用户提供的迭代器转换为标准形式:
- 如果
iteratee
是undefined
(没有提供迭代器或最后一个参数是数组),则使用默认的_.identity
迭代器 - 如果
iteratee
是字符串,则转换为属性访问器函数 - 如果
iteratee
是函数,则直接使用该函数 - 参数
2
表示迭代器的参数个数上限
这种灵活性允许用户使用多种形式的迭代器:
js
// 使用函数作为迭代器
_.differenceBy([2.1, 1.2], [2.3, 3.4], Math.floor);
// 使用字符串属性作为迭代器(简写形式)
_.differenceBy([{ x: 2 }, { x: 1 }], [{ x: 1 }], "x");
6. 差集计算
js
baseDifference(
array,
baseFlatten(values, 1, isArrayLikeObject, true),
getIteratee(iteratee, 2)
);
最后,baseDifference
函数执行实际的差集计算:
- 第一个参数是原始数组
- 第二个参数是扁平化后的排除值数组
- 第三个参数是处理后的迭代器函数
baseDifference
会对每个元素应用迭代器函数,然后比较转换后的值,返回仅在第一个数组中存在的元素。
总结
differenceBy
函数是 Lodash 中灵活且强大的数组差集计算工具,其核心优势在于:
- 灵活的参数处理:可以接受任意数量的数组参数和一个可选的迭代器函数
- 多种迭代器形式:支持函数迭代器和字符串属性简写
- 健壮的边缘情况处理:安全处理非数组输入和特殊参数形式
- 高效的实现 :内部使用优化的
baseDifference
实现,确保性能 - 直观的 API 设计:迭代器位置的自动推断,减少使用障碍
这种设计展示了函数式编程中参数处理、类型检查和函数组合的最佳实践,是学习 JavaScript 函数式编程的优秀范例。