Lodash 源码阅读-equalObjects
概述
equalObjects
是 Lodash 中用于深度比较两个对象是否相等的内部函数。它是 baseIsEqualDeep
的特化版本,专门用于处理对象类型的比较,支持部分深度比较和循环引用检测。
前置学习
依赖函数
getAllKeys
:获取对象自身的可枚举属性名和符号的数组Stack
:用于跟踪已遍历对象的栈结构,防止循环引用导致的无限递归equalFunc
:通常是baseIsEqual
,用于递归比较对象属性值COMPARE_PARTIAL_FLAG
:值为 1 的位掩码常量,表示部分比较模式COMPARE_UNORDERED_FLAG
:值为 2 的位掩码常量,表示无序比较模式
技术知识
- 位运算:使用位掩码标志控制比较行为
- 循环引用检测:使用 Stack 数据结构跟踪已比较的对象
- 对象属性遍历:使用
getAllKeys
获取对象的所有可枚举属性 - 构造函数比较:检查非 Object 对象实例的构造函数是否相同
源码实现
javascript
function equalObjects(object, other, bitmask, customizer, equalFunc, stack) {
var isPartial = bitmask & COMPARE_PARTIAL_FLAG,
objProps = getAllKeys(object),
objLength = objProps.length,
othProps = getAllKeys(other),
othLength = othProps.length;
if (objLength != othLength && !isPartial) {
return false;
}
var index = objLength;
while (index--) {
var key = objProps[index];
if (!(isPartial ? key in other : hasOwnProperty.call(other, key))) {
return false;
}
}
// Check that cyclic values are equal.
var objStacked = stack.get(object);
var othStacked = stack.get(other);
if (objStacked && othStacked) {
return objStacked == other && othStacked == object;
}
var result = true;
stack.set(object, other);
stack.set(other, object);
var skipCtor = isPartial;
while (++index < objLength) {
key = objProps[index];
var objValue = object[key],
othValue = other[key];
if (customizer) {
var compared = isPartial
? customizer(othValue, objValue, key, other, object, stack)
: customizer(objValue, othValue, key, object, other, stack);
}
// Recursively compare objects (susceptible to call stack limits).
if (
!(compared === undefined
? objValue === othValue ||
equalFunc(objValue, othValue, bitmask, customizer, stack)
: compared)
) {
result = false;
break;
}
skipCtor || (skipCtor = key == "constructor");
}
if (result && !skipCtor) {
var objCtor = object.constructor,
othCtor = other.constructor;
// Non `Object` object instances with different constructors are not equal.
if (
objCtor != othCtor &&
"constructor" in object &&
"constructor" in other &&
!(
typeof objCtor == "function" &&
objCtor instanceof objCtor &&
typeof othCtor == "function" &&
othCtor instanceof othCtor
)
) {
result = false;
}
}
stack["delete"](object);
stack["delete"](other);
return result;
}
实现思路
equalObjects
函数通过以下步骤比较两个对象是否相等:
- 首先获取两个对象的所有可枚举属性(包括符号属性)
- 检查属性数量是否相同(在非部分比较模式下)
- 确保第一个对象的所有属性在第二个对象中都存在
- 检测并处理循环引用情况
- 逐个比较每个属性的值是否相等
- 如果所有属性值都相等,还会检查对象的构造函数是否相同(除非跳过构造函数检查)
整个过程中,函数会根据比较模式(部分比较、自定义比较器等)调整比较行为,并使用 Stack 数据结构防止循环引用导致的无限递归。
源码解析
初始化和属性数量检查
javascript
var isPartial = bitmask & COMPARE_PARTIAL_FLAG,
objProps = getAllKeys(object),
objLength = objProps.length,
othProps = getAllKeys(other),
othLength = othProps.length;
if (objLength != othLength && !isPartial) {
return false;
}
首先,函数通过位运算 bitmask & COMPARE_PARTIAL_FLAG
检查是否为部分比较模式。COMPARE_PARTIAL_FLAG
的值为 1,如果 bitmask
的最低位为 1,则 isPartial
为 true。
然后,使用 getAllKeys
获取两个对象的所有可枚举属性(包括符号属性)。在非部分比较模式下,如果两个对象的属性数量不同,则直接返回 false。
例如:
javascript
const obj1 = { a: 1, b: 2 };
const obj2 = { a: 1, b: 2, c: 3 };
// 在非部分比较模式下,这两个对象会被认为不相等
// 在部分比较模式下,只要 obj1 的所有属性在 obj2 中都存在且值相等,就认为相等
属性存在性检查
javascript
var index = objLength;
while (index--) {
var key = objProps[index];
if (!(isPartial ? key in other : hasOwnProperty.call(other, key))) {
return false;
}
}
这段代码检查第一个对象的所有属性是否在第二个对象中存在。根据比较模式的不同,检查方式也不同:
- 在部分比较模式下,使用
key in other
检查属性是否存在(包括继承属性) - 在非部分比较模式下,使用
hasOwnProperty.call(other, key)
只检查自有属性
不同检查方式的原因
-
部分比较模式(isPartial = true)
- 使用
key in other
的原因:- 允许检查继承属性,更宽松的比较方式
- 适用于只需要确保目标对象具有源对象的所有属性,而不关心属性来源的场景
- 例如:检查一个对象是否满足接口要求,即使属性是通过原型继承获得的
- 使用
-
非部分比较模式(isPartial = false)
- 使用
hasOwnProperty.call(other, key)
的原因:- 只检查自有属性,更严格的比较方式
- 确保两个对象具有完全相同的属性结构,包括属性的来源
- 避免因原型链上的属性导致误判
- 例如:深度克隆对象时,需要确保完全相同的属性结构
- 使用
示例说明
javascript
// 部分比较模式示例
const source = { name: "test" };
const target = Object.create({ name: "test" }); // 通过原型继承 name 属性
_.isEqual(source, target); // false(非部分比较)
_.isEqualWith(source, target, (value, other) => {
if (value === "test" && other === "test") return true;
return undefined; // 使用默认比较
}); // true(部分比较)
// 非部分比较模式示例
const obj1 = { name: "test" };
const obj2 = Object.create({ name: "test" });
_.isEqual(obj1, obj2); // false,因为 name 属性来源不同
循环引用检测
javascript
var objStacked = stack.get(object);
var othStacked = stack.get(other);
if (objStacked && othStacked) {
return objStacked == other && othStacked == object;
}
var result = true;
stack.set(object, other);
stack.set(other, object);
这部分代码处理循环引用情况。如果 object
和 other
已经在 stack
中(表示之前已经比较过),则检查它们是否互相引用。如果是互相引用的循环结构,则认为它们相等。
然后,将当前比较的对象对放入 stack
中,以便后续检测循环引用。
例如:
javascript
const obj1 = {};
const obj2 = {};
obj1.self = obj1;
obj2.self = obj2;
// 这两个对象都有循环引用,但它们的结构相同,应该被认为是相等的
属性值比较
javascript
var skipCtor = isPartial;
while (++index < objLength) {
key = objProps[index];
var objValue = object[key],
othValue = other[key];
if (customizer) {
var compared = isPartial
? customizer(othValue, objValue, key, other, object, stack)
: customizer(objValue, othValue, key, object, other, stack);
}
// Recursively compare objects (susceptible to call stack limits).
if (
!(compared === undefined
? objValue === othValue ||
equalFunc(objValue, othValue, bitmask, customizer, stack)
: compared)
) {
result = false;
break;
}
skipCtor || (skipCtor = key == "constructor");
}
这段代码逐个比较对象的属性值。如果提供了自定义比较器 customizer
,则使用它进行比较。注意在部分比较模式下,参数顺序会交换。
如果自定义比较器返回 undefined
,则使用默认比较逻辑:
- 先尝试使用
===
进行严格相等比较 - 如果不相等,则使用
equalFunc
(通常是baseIsEqual
)递归比较
如果任何属性值不相等,则设置 result = false
并跳出循环。
同时,代码还检查是否遇到了 constructor
属性,如果遇到了,则设置 skipCtor = true
,表示后续不需要再比较构造函数。
构造函数比较
javascript
if (result && !skipCtor) {
var objCtor = object.constructor,
othCtor = other.constructor;
// Non `Object` object instances with different constructors are not equal.
if (
objCtor != othCtor &&
"constructor" in object &&
"constructor" in other &&
!(
typeof objCtor == "function" &&
objCtor instanceof objCtor &&
typeof othCtor == "function" &&
othCtor instanceof othCtor
)
) {
result = false;
}
}
如果所有属性值都相等且没有跳过构造函数检查,则比较两个对象的构造函数。这主要是为了处理自定义类的实例。
构造函数比较的逻辑比较复杂:
- 首先检查两个对象的构造函数是否不同
- 确保两个对象都有
constructor
属性 - 最后检查构造函数本身是否是有效的构造函数(通过
constructor instanceof constructor
判断)
例如:
javascript
class A {}
class B {}
const a = new A();
const b = new B();
// 虽然 a 和 b 可能有相同的属性,但它们的构造函数不同,应该被认为不相等
清理和返回结果
javascript
stack["delete"](object);
stack["delete"](other);
return result;
最后,从 stack
中删除当前比较的对象对,以避免内存泄漏,并返回比较结果。
总结
equalObjects
函数是 Lodash 深度相等比较系统中的核心组件,专门负责对象类型的比较。它的设计体现了几个重要的软件工程原则:
-
单一职责原则 :
equalObjects
只负责对象比较,而数组比较由equalArrays
处理,其他类型由equalByTag
处理。 -
可扩展性 :通过
customizer
参数支持自定义比较逻辑,使比较行为可以根据需求定制。 -
健壮性:通过 Stack 数据结构处理循环引用,防止无限递归导致的栈溢出。
-
灵活性:通过位掩码标志支持不同的比较模式(部分比较、无序比较等)。
equalObjects
的实现也展示了处理复杂对象比较的最佳实践:
- 先比较简单属性(如属性数量)
- 检查属性存在性
- 处理循环引用
- 递归比较属性值
- 考虑特殊情况(如构造函数比较)
这些技术在需要进行深度对象比较的场景中非常有用,可以帮助我们编写更健壮、更可靠的代码。