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

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

相关推荐
七灵微2 小时前
ES6入门---第三单元 模块三:async、await
前端·javascript·es6
七灵微4 小时前
ES6入门---第二单元 模块五:模块化
前端·ecmascript·es6
m0_616188495 小时前
vue3 - keepAlive缓存组件
前端·vue.js·缓存
lh_12546 小时前
Uni-app 组件使用
前端·javascript·uni-app
Kx…………6 小时前
Day3:设置页面全局渐变线性渐变背景色uniapp壁纸实战
前端·学习·uni-app·实战·项目
Q_Boom6 小时前
前端跨域问题怎么在后端解决
java·前端·后端·spring
搬砖工程师Cola6 小时前
<Revit二次开发> 通过一组模型线构成墙面,并生成墙。Create(Document, IList.Curve., Boolean)
java·前端·javascript
林十一npc6 小时前
Fiddler抓取APP端,HTTPS报错全解析及解决方案(一篇解决常见问题)
android·前端·网络协议·https·fiddler·接口测试
小妖6667 小时前
4个纯CSS自定义的简单而优雅的滚动条样式
前端·javascript·css
Yensean7 小时前
Learning vtkjs之MultiSliceImageMapper
javascript·webgl