Lodash源码阅读-baseIsEqualDeep

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

  1. 首先确定两个值的类型标签,并进行标准化处理(如将 Arguments 对象视为普通对象)
  2. 根据类型标签选择合适的比较策略:
    • 对于数组和类型化数组,使用 equalArrays 函数
    • 对于具有特定标签的对象(如 Date、RegExp 等),使用 equalByTag 函数
    • 对于普通对象,使用 equalObjects 函数
  3. 处理特殊情况,如 Buffer 对象和包装对象(如 Lodash 包装的对象)
  4. 使用 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 设为 trueobjIsObj 设为 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 数据类型的精确比较。其设计体现了几个重要的软件工程原则:

  1. 单一职责原则 :将不同类型的比较逻辑分离到专门的函数中(equalArraysequalByTagequalObjects)。

  2. 开放封闭原则 :通过 customizer 参数支持自定义比较逻辑,使比较行为可以扩展而无需修改核心代码。

  3. 策略模式:根据值的类型选择不同的比较策略,使代码更加清晰和可维护。

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

baseIsEqualDeep 的实现也展示了处理复杂数据比较的最佳实践:

  1. 先进行类型检查,快速排除不可能相等的情况
  2. 根据类型选择专门的比较策略
  3. 处理特殊情况和边缘情况
  4. 使用适当的数据结构防止循环引用问题

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

相关推荐
qq. 28040339843 小时前
CSS层叠顺序
前端·css
喝拿铁写前端3 小时前
SmartField AI:让每个字段都找到归属!
前端·算法
猫猫不是喵喵.3 小时前
vue 路由
前端·javascript·vue.js
烛阴4 小时前
JavaScript Import/Export:告别混乱,拥抱模块化!
前端·javascript
bin91534 小时前
DeepSeek 助力 Vue3 开发:打造丝滑的表格(Table)之添加行拖拽排序功能示例12,TableView16_12 拖拽动画示例
前端·javascript·vue.js·ecmascript·deepseek
GISer_Jing4 小时前
[Html]overflow: auto 失效原因,flex 1却未设置min-height &overflow的几个属性以及应用场景
前端·html
程序员黄同学4 小时前
解释 Webpack 中的模块打包机制,如何配置 Webpack 进行项目构建?
前端·webpack·node.js
拉不动的猪4 小时前
vue自定义“权限控制”指令
前端·javascript·vue.js
再学一点就睡4 小时前
浏览器页面渲染机制深度解析:从构建 DOM 到 transform 高效渲染的底层逻辑
前端·css
拉不动的猪5 小时前
刷刷题48 (setState常规问答)
前端·react.js·面试