Lodash 源码阅读-baseIsEqual
概述
baseIsEqual
是 Lodash 中实现"深度相等比较"的核心函数。说白了,它就是用来判断两个值是不是"真的相等",不管是简单的数字字符串,还是复杂的嵌套对象和数组,甚至是那些环形引用的结构,它都能正确处理。这个函数是 _.isEqual
和 _.isEqualWith
这两个公开 API 的内部实现基础。
前置学习
依赖函数
baseIsEqualDeep
:处理复杂数据类型的深度比较,比如对象、数组等需要逐项比较的情况isObjectLike
:判断一个值是不是"像对象"(typeof 是 'object' 但不是 null)
技术知识
- 严格相等比较 :JavaScript 中的
===
操作符,判断两个值是否严格相同 - NaN 特殊处理 :JS 中
NaN !== NaN
这个奇葩特性及其处理方法 - 短路优化:通过先判断简单情况来避免不必要的复杂计算
- 位掩码:使用二进制位来控制函数行为的技巧
- 递归比较:如何安全地处理嵌套数据结构的比较
- 循环引用检测:如何避免因循环引用导致的无限递归
源码实现
javascript
function baseIsEqual(value, other, bitmask, customizer, stack) {
if (value === other) {
return true;
}
if (
value == null ||
other == null ||
(!isObjectLike(value) && !isObjectLike(other))
) {
return value !== value && other !== other;
}
return baseIsEqualDeep(value, other, bitmask, customizer, baseIsEqual, stack);
}
实现思路
baseIsEqual
的思路很聪明,它采用了"分层检查"的策略:
- 先看最简单的情况:如果两个值严格相等(用
===
判断),那就直接返回true
- 再处理一些特殊情况:比如有一个值是
null
、两个值都不是对象等,这时只有在两个值都是NaN
的情况下才返回true
- 最后,对于需要深入比较的复杂数据类型(比如对象、数组),就交给专门的
baseIsEqualDeep
函数处理
源码解析
参数说明
javascript
function baseIsEqual(value, other, bitmask, customizer, stack) {
// ...
}
这个函数接收五个参数:
value
和other
:要比较的两个值bitmask
:控制比较行为的位掩码,是一个数字,里面的每一位都有特定含义:1
(COMPARE_UNORDERED_FLAG):表示无序比较,主要用于对象属性顺序不重要的场景2
(COMPARE_PARTIAL_FLAG):表示部分比较,允许other
是value
的子集- 组合使用如
3
(1|2)表示既无序又部分比较
customizer
:自定义比较函数,让用户可以定义特殊的比较逻辑stack
:用于跟踪已比较的对象,防止循环引用导致的无限递归
严格相等比较 - 快速路径
javascript
if (value === other) {
return true;
}
这是最简单也是最快的检查。对于大多数基本类型(数字、字符串、布尔值等)和引用相同的对象,这一步就能得出结果,避免了后续更复杂的比较。
举几个例子:
javascript
// 这些情况在第一步就会返回 true
baseIsEqual(1, 1); // 基本类型相同
baseIsEqual("hello", "hello"); // 字符串相同
baseIsEqual(true, true); // 布尔值相同
let obj = {};
baseIsEqual(obj, obj); // 同一个对象引用
特殊情况处理 - NaN 比较
javascript
if (
value == null ||
other == null ||
(!isObjectLike(value) && !isObjectLike(other))
) {
return value !== value && other !== other;
}
这段代码看着复杂,其实就是在判断:
- 如果有一个值是
null
或undefined
- 或者两个值都不是对象类型
在这些情况下,我们只在一种特殊情况下返回 true
:两个值都是 NaN
。
为什么要这么写?因为在 JavaScript 中,NaN
有个奇葩的特性:它不等于自己!也就是 NaN !== NaN
会返回 true
。所以 value !== value
实际上是在判断 value
是不是 NaN
。
javascript
// NaN 的特殊处理
baseIsEqual(NaN, NaN); // true,特殊处理让 NaN 等于 NaN
baseIsEqual(null, undefined); // false,尽管它们双等号(==)为true,但这里不相等
baseIsEqual(42, "42"); // false,不会自动类型转换
深度比较 - 复杂对象处理
javascript
return baseIsEqualDeep(value, other, bitmask, customizer, baseIsEqual, stack);
当走到这一步,说明我们需要进行深度比较了。函数将任务委托给 baseIsEqualDeep
,同时传入几个重要参数:
- 要比较的两个值:
value
和other
- 控制比较行为的位掩码:
bitmask
- 自定义比较函数:
customizer
- 递归比较的函数:
baseIsEqual
(就是它自己,用于后续递归) - 已比较对象的跟踪栈:
stack
,防止循环引用导致无限递归
baseIsEqualDeep
会根据值的类型使用不同的策略:
- 对于数组,使用
equalArrays
逐项比较 - 对于对象,使用
equalObjects
比较每个属性 - 对于其他类型(如 Date、RegExp 等),使用
equalByTag
根据其 toString 标签选择合适的比较方法
Stack 参数的作用
stack
参数很关键,它是用来处理循环引用的关键机制。想象一下,如果有两个对象互相引用:
javascript
const obj1 = {};
const obj2 = {};
obj1.ref = obj2;
obj2.ref = obj1;
// 如果没有 stack 参数,这里会无限递归
baseIsEqual(obj1, obj2);
为了避免无限递归,stack
会记录已经比较过的对象对,如果发现当前比较的对象对之前已经比较过,就直接返回之前的比较结果,而不是再次递归比较。
javascript
// stack 的大致工作原理
function compareWithStack(a, b, stack = new Map()) {
// 检查是否已经比较过这对对象
if (stack.has(a)) {
return stack.get(a) === b;
}
// 记录正在比较的对象对
stack.set(a, b);
// 进行实际比较...
// 比较完成后可以从 stack 中移除
stack.delete(a);
}
总结
baseIsEqual
是 Lodash 深度比较功能的基石,它解决了 JavaScript 原生比较操作符的局限性。它的精彩之处在于:
- 巧妙的分层设计:从简单比较开始,只在必要时才进行复杂比较,大大提高了性能
- 灵活的比较策略:通过位掩码控制比较行为,支持有序/无序、完全/部分多种比较模式
- 全面的类型处理:不仅处理基本类型,还能正确处理各种复杂的对象类型
- 安全的递归实现:通过 Stack 机制避免循环引用导致的无限递归
- 可扩展的接口:通过 customizer 允许用户自定义比较逻辑
虽然这只是 Lodash 的一个内部函数,但从它的设计中我们可以学到很多实用技巧:优先处理常见情况、特殊处理边缘案例、分解复杂问题、提供灵活的扩展点。这些思想不仅适用于相等比较,也可以应用到其他复杂算法的设计中。