Lodash源码阅读-equalObjects

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 函数通过以下步骤比较两个对象是否相等:

  1. 首先获取两个对象的所有可枚举属性(包括符号属性)
  2. 检查属性数量是否相同(在非部分比较模式下)
  3. 确保第一个对象的所有属性在第二个对象中都存在
  4. 检测并处理循环引用情况
  5. 逐个比较每个属性的值是否相等
  6. 如果所有属性值都相等,还会检查对象的构造函数是否相同(除非跳过构造函数检查)

整个过程中,函数会根据比较模式(部分比较、自定义比较器等)调整比较行为,并使用 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) 只检查自有属性
不同检查方式的原因
  1. 部分比较模式(isPartial = true)

    • 使用 key in other 的原因:
      • 允许检查继承属性,更宽松的比较方式
      • 适用于只需要确保目标对象具有源对象的所有属性,而不关心属性来源的场景
      • 例如:检查一个对象是否满足接口要求,即使属性是通过原型继承获得的
  2. 非部分比较模式(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);

这部分代码处理循环引用情况。如果 objectother 已经在 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,则使用默认比较逻辑:

  1. 先尝试使用 === 进行严格相等比较
  2. 如果不相等,则使用 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;
  }
}

如果所有属性值都相等且没有跳过构造函数检查,则比较两个对象的构造函数。这主要是为了处理自定义类的实例。

构造函数比较的逻辑比较复杂:

  1. 首先检查两个对象的构造函数是否不同
  2. 确保两个对象都有 constructor 属性
  3. 最后检查构造函数本身是否是有效的构造函数(通过 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 深度相等比较系统中的核心组件,专门负责对象类型的比较。它的设计体现了几个重要的软件工程原则:

  1. 单一职责原则equalObjects 只负责对象比较,而数组比较由 equalArrays 处理,其他类型由 equalByTag 处理。

  2. 可扩展性 :通过 customizer 参数支持自定义比较逻辑,使比较行为可以根据需求定制。

  3. 健壮性:通过 Stack 数据结构处理循环引用,防止无限递归导致的栈溢出。

  4. 灵活性:通过位掩码标志支持不同的比较模式(部分比较、无序比较等)。

equalObjects 的实现也展示了处理复杂对象比较的最佳实践:

  1. 先比较简单属性(如属性数量)
  2. 检查属性存在性
  3. 处理循环引用
  4. 递归比较属性值
  5. 考虑特殊情况(如构造函数比较)

这些技术在需要进行深度对象比较的场景中非常有用,可以帮助我们编写更健壮、更可靠的代码。

相关推荐
codingandsleeping3 分钟前
浏览器的缓存机制
前端·后端
-代号952736 分钟前
【JavaScript】十二、定时器
开发语言·javascript·ecmascript
灵感__idea2 小时前
JavaScript高级程序设计(第5版):扎实的基本功是唯一捷径
前端·javascript·程序员
摇滚侠2 小时前
Vue3 其它API toRow和markRow
前端·javascript
難釋懷2 小时前
JavaScript基础-history 对象
开发语言·前端·javascript
beibeibeiooo2 小时前
【CSS3】04-标准流 + 浮动 + flex布局
前端·html·css3
拉不动的猪2 小时前
刷刷题47(react常规面试题2)
前端·javascript·面试
浪遏2 小时前
场景题:大文件上传 ?| 过总字节一面😱
前端·javascript·面试
计算机毕设定制辅导-无忧学长2 小时前
HTML 与 JavaScript 交互:学习进程中的新跨越(一)
javascript·html·交互
Bigger2 小时前
Tauri(十八)——如何开发 Tauri 插件
前端·rust·app