Lodash 源码阅读-baseIsEqualDeep
概述
baseIsEqualDeep
是 Lodash 中深度比较两个值是否相等的核心函数,它是 baseIsEqual
的专用版本,专门处理需要深度比较的复杂数据类型(如数组、对象等)。该函数能够处理循环引用,并支持自定义比较逻辑,是 Lodash 中 _.isEqual
方法的核心实现。
前置学习
依赖函数
isArray
:检查值是否为数组getTag
/baseGetTag
:获取值的内部[[Class]]
标签isBuffer
:检查值是否为 Buffer 对象isTypedArray
:检查值是否为类型化数组equalArrays
:比较两个数组是否相等equalByTag
:根据对象的标签类型进行特定比较equalObjects
:比较两个对象是否相等Stack
:用于跟踪已遍历对象的栈结构,防止循环引用导致的无限递归
技术知识
- 类型标签:JavaScript 内部使用
[[Class]]
属性标识对象类型 - 位运算:使用位掩码标志控制比较行为
- 循环引用检测:使用 Stack 数据结构跟踪已比较的对象
- 多态比较:根据值的类型选择不同的比较策略
源码实现
javascript
function baseIsEqualDeep(object, other, bitmask, customizer, equalFunc, stack) {
var objIsArr = isArray(object),
othIsArr = isArray(other),
objTag = objIsArr ? arrayTag : getTag(object),
othTag = othIsArr ? arrayTag : getTag(other);
objTag = objTag == argsTag ? objectTag : objTag;
othTag = othTag == argsTag ? objectTag : othTag;
var objIsObj = objTag == objectTag,
othIsObj = othTag == objectTag,
isSameTag = objTag == othTag;
if (isSameTag && isBuffer(object)) {
if (!isBuffer(other)) {
return false;
}
objIsArr = true;
objIsObj = false;
}
if (isSameTag && !objIsObj) {
stack || (stack = new Stack());
return objIsArr || isTypedArray(object)
? equalArrays(object, other, bitmask, customizer, equalFunc, stack)
: equalByTag(
object,
other,
objTag,
bitmask,
customizer,
equalFunc,
stack
);
}
if (!(bitmask & COMPARE_PARTIAL_FLAG)) {
var objIsWrapped = objIsObj && hasOwnProperty.call(object, "__wrapped__"),
othIsWrapped = othIsObj && hasOwnProperty.call(other, "__wrapped__");
if (objIsWrapped || othIsWrapped) {
var objUnwrapped = objIsWrapped ? object.value() : object,
othUnwrapped = othIsWrapped ? other.value() : other;
stack || (stack = new Stack());
return equalFunc(objUnwrapped, othUnwrapped, bitmask, customizer, stack);
}
}
if (!isSameTag) {
return false;
}
stack || (stack = new Stack());
return equalObjects(object, other, bitmask, customizer, equalFunc, stack);
}
实现思路
baseIsEqualDeep
函数通过以下步骤比较两个值是否深度相等:
- 首先确定两个值的类型标签,并进行标准化处理(如将 Arguments 对象视为普通对象)
- 根据类型标签选择合适的比较策略:
- 对于数组和类型化数组,使用
equalArrays
函数 - 对于具有特定标签的对象(如 Date、RegExp 等),使用
equalByTag
函数 - 对于普通对象,使用
equalObjects
函数
- 对于数组和类型化数组,使用
- 处理特殊情况,如 Buffer 对象和包装对象(如 Lodash 包装的对象)
- 使用 Stack 数据结构跟踪已比较的对象,防止循环引用导致的无限递归
整个过程中,函数会根据比较模式(部分比较、自定义比较器等)调整比较行为,确保在各种复杂情况下都能正确比较。
源码解析
类型检测和标签获取
javascript
var objIsArr = isArray(object),
othIsArr = isArray(other),
objTag = objIsArr ? arrayTag : getTag(object),
othTag = othIsArr ? arrayTag : getTag(other);
objTag = objTag == argsTag ? objectTag : objTag;
othTag = othTag == argsTag ? objectTag : othTag;
首先,函数检查两个值是否为数组,并获取它们的类型标签。这里有一个优化:如果值是数组,直接使用 arrayTag
(即 [object Array]
),避免调用 getTag
函数。
然后,函数将 Arguments 对象的标签([object Arguments]
)转换为普通对象的标签([object Object]
),这是为了简化后续的比较逻辑,将 Arguments 对象视为普通对象处理。
类型标签比较和特殊处理
javascript
var objIsObj = objTag == objectTag,
othIsObj = othTag == objectTag,
isSameTag = objTag == othTag;
if (isSameTag && isBuffer(object)) {
if (!isBuffer(other)) {
return false;
}
objIsArr = true;
objIsObj = false;
}
接下来,函数检查两个值是否都是普通对象,以及它们的类型标签是否相同。
对于 Buffer 对象,需要特殊处理:如果第一个值是 Buffer,但第二个值不是,则直接返回 false
;如果两者都是 Buffer,则将 objIsArr
设为 true
,objIsObj
设为 false
,这样后续会将它们作为数组处理。
数组和特定类型对象的比较
javascript
if (isSameTag && !objIsObj) {
stack || (stack = new Stack());
return objIsArr || isTypedArray(object)
? equalArrays(object, other, bitmask, customizer, equalFunc, stack)
: equalByTag(object, other, objTag, bitmask, customizer, equalFunc, stack);
}
如果两个值的类型标签相同,且不是普通对象,则根据类型选择不同的比较策略:
- 对于数组和类型化数组,使用
equalArrays
函数 - 对于其他特定类型(如 Date、RegExp、Symbol 等),使用
equalByTag
函数
这里首次使用 Stack 数据结构,用于跟踪已比较的对象,防止循环引用导致的无限递归。
包装对象处理
javascript
if (!(bitmask & COMPARE_PARTIAL_FLAG)) {
var objIsWrapped = objIsObj && hasOwnProperty.call(object, "__wrapped__"),
othIsWrapped = othIsObj && hasOwnProperty.call(other, "__wrapped__");
if (objIsWrapped || othIsWrapped) {
var objUnwrapped = objIsWrapped ? object.value() : object,
othUnwrapped = othIsWrapped ? other.value() : other;
stack || (stack = new Stack());
return equalFunc(objUnwrapped, othUnwrapped, bitmask, customizer, stack);
}
}
在非部分比较模式下(COMPARE_PARTIAL_FLAG
为 1),函数检查两个值是否为 Lodash 包装对象(具有 __wrapped__
属性)。如果是,则解包并比较它们的实际值。
这是为了处理 Lodash 链式调用中的包装对象,确保 _(1)
和 _(1)
被视为相等。
不同类型标签的处理
javascript
if (!isSameTag) {
return false;
}
如果两个值的类型标签不同,则直接返回 false
。这是一个快速失败的优化,因为不同类型的值通常不相等。
普通对象的比较
javascript
stack || (stack = new Stack());
return equalObjects(object, other, bitmask, customizer, equalFunc, stack);
最后,对于普通对象(包括被转换为普通对象的 Arguments 对象),使用 equalObjects
函数进行深度比较。
总结
baseIsEqualDeep
函数是 Lodash 深度相等比较系统的核心组件,它通过多态比较策略和循环引用检测,实现了对各种 JavaScript 数据类型的精确比较。其设计体现了几个重要的软件工程原则:
-
单一职责原则 :将不同类型的比较逻辑分离到专门的函数中(
equalArrays
、equalByTag
、equalObjects
)。 -
开放封闭原则 :通过
customizer
参数支持自定义比较逻辑,使比较行为可以扩展而无需修改核心代码。 -
策略模式:根据值的类型选择不同的比较策略,使代码更加清晰和可维护。
-
健壮性:通过 Stack 数据结构处理循环引用,防止无限递归导致的栈溢出。
baseIsEqualDeep
的实现也展示了处理复杂数据比较的最佳实践:
- 先进行类型检查,快速排除不可能相等的情况
- 根据类型选择专门的比较策略
- 处理特殊情况和边缘情况
- 使用适当的数据结构防止循环引用问题
这些技术在需要进行深度数据比较的场景中非常有用,可以帮助我们编写更健壮、更可靠的代码。