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. 使用适当的数据结构防止循环引用问题

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

相关推荐
xjt_09011 小时前
基于 Vue 3 构建企业级 Web Components 组件库
前端·javascript·vue.js
我是伪码农2 小时前
Vue 2.3
前端·javascript·vue.js
夜郎king2 小时前
HTML5 SVG 实现日出日落动画与实时天气可视化
前端·html5·svg 日出日落
辰风沐阳2 小时前
JavaScript 的宏任务和微任务
javascript
夏幻灵3 小时前
HTML5里最常用的十大标签
前端·html·html5
冰暮流星3 小时前
javascript之二重循环练习
开发语言·javascript·数据库
Mr Xu_3 小时前
Vue 3 中 watch 的使用详解:监听响应式数据变化的利器
前端·javascript·vue.js
未来龙皇小蓝3 小时前
RBAC前端架构-01:项目初始化
前端·架构
程序员agions4 小时前
2026年,微前端终于“死“了
前端·状态模式
万岳科技系统开发4 小时前
食堂采购系统源码库存扣减算法与并发控制实现详解
java·前端·数据库·算法