Lodash源码阅读-equalByTag

Lodash 源码阅读-equalByTag

功能概述

equalByTag 是 Lodash 库中的一个内部工具函数,专门用于比较具有相同 toStringTag 的对象是否相等。它是深度相等比较(_.isEqual)的核心组件之一,负责处理特定类型对象的比较逻辑。这个函数能够处理多种 JavaScript 内置类型,包括 BooleanDateErrorNumberRegExpStringMapSetArrayBufferDataViewSymbol 等。

前置学习

依赖函数

equalByTag 依赖以下几个 Lodash 函数:

  • eq:用于基本类型的相等比较
  • mapToArray:将 Map 对象转换为键值对数组
  • setToArray:将 Set 对象转换为值数组
  • equalArrays:比较两个数组是否相等

技术知识

  • Object.prototype.toString.call :获取对象的内部 [[Class]] 属性,即 toStringTag
  • 位掩码(Bitmask):使用二进制位表示多个布尔标志
  • 类型转换:如何在比较前对不同类型的值进行适当的转换
  • 循环引用处理:如何处理对象中的循环引用问题
  • 特殊类型比较:如何比较特殊类型如 ArrayBuffer、DataView 等

源码实现

javascript 复制代码
/**
 * A specialized version of `baseIsEqualDeep` for comparing objects of
 * the same `toStringTag`.
 *
 * **Note:** This function only supports comparing values with tags of
 * `Boolean`, `Date`, `Error`, `Number`, `RegExp`, or `String`.
 *
 * @private
 * @param {Object} object The object to compare.
 * @param {Object} other The other object to compare.
 * @param {string} tag The `toStringTag` of the objects to compare.
 * @param {number} bitmask The bitmask flags. See `baseIsEqual` for more details.
 * @param {Function} customizer The function to customize comparisons.
 * @param {Function} equalFunc The function to determine equivalents of values.
 * @param {Object} stack Tracks traversed `object` and `other` objects.
 * @returns {boolean} Returns `true` if the objects are equivalent, else `false`.
 */
function equalByTag(object, other, tag, bitmask, customizer, equalFunc, stack) {
  switch (tag) {
    case dataViewTag:
      if (
        object.byteLength != other.byteLength ||
        object.byteOffset != other.byteOffset
      ) {
        return false;
      }
      object = object.buffer;
      other = other.buffer;

    case arrayBufferTag:
      if (
        object.byteLength != other.byteLength ||
        !equalFunc(new Uint8Array(object), new Uint8Array(other))
      ) {
        return false;
      }
      return true;

    case boolTag:
    case dateTag:
    case numberTag:
      // Coerce booleans to `1` or `0` and dates to milliseconds.
      // Invalid dates are coerced to `NaN`.
      return eq(+object, +other);

    case errorTag:
      return object.name == other.name && object.message == other.message;

    case regexpTag:
    case stringTag:
      // Coerce regexes to strings and treat strings, primitives and objects,
      // as equal. See http://www.ecma-international.org/ecma-262/7.0/#sec-regexp.prototype.tostring
      // for more details.
      return object == other + "";

    case mapTag:
      var convert = mapToArray;

    case setTag:
      var isPartial = bitmask & COMPARE_PARTIAL_FLAG;
      convert || (convert = setToArray);

      if (object.size != other.size && !isPartial) {
        return false;
      }
      // Assume cyclic values are equal.
      var stacked = stack.get(object);
      if (stacked) {
        return stacked == other;
      }
      bitmask |= COMPARE_UNORDERED_FLAG;

      // Recursively compare objects (susceptible to call stack limits).
      stack.set(object, other);
      var result = equalArrays(
        convert(object),
        convert(other),
        bitmask,
        customizer,
        equalFunc,
        stack
      );
      stack["delete"](object);
      return result;

    case symbolTag:
      if (symbolValueOf) {
        return symbolValueOf.call(object) == symbolValueOf.call(other);
      }
  }
  return false;
}

实现思路

equalByTag 函数的实现思路是根据对象的 toStringTag(即对象的内部类型标识)选择适当的比较策略:

  1. 类型分发 :使用 switch 语句根据对象的 tag 选择相应的比较逻辑
  2. 特殊处理:对不同类型的对象采用不同的比较方法
  3. 类型转换:在比较前对某些类型进行适当的转换,以简化比较逻辑
  4. 递归比较:对于复杂类型如 Map 和 Set,将它们转换为数组后递归比较
  5. 循环引用处理 :使用 stack 对象跟踪已比较的对象,防止无限递归

这种实现方式既高效又灵活,能够处理各种 JavaScript 内置类型的深度比较。

源码解析

参数解析

javascript 复制代码
function equalByTag(object, other, tag, bitmask, customizer, equalFunc, stack) {

函数接收七个参数:

  • object:要比较的第一个对象
  • other:要比较的第二个对象
  • tag:对象的 toStringTag,表示对象的类型
  • bitmask:位掩码标志,控制比较的行为
    • COMPARE_PARTIAL_FLAG (1):部分比较模式
    • COMPARE_UNORDERED_FLAG (2):无序比较模式
  • customizer:自定义比较函数,允许用户定制比较逻辑
  • equalFunc:用于确定值是否相等的函数,通常是 baseIsEqual
  • stack:用于跟踪已遍历的对象,防止循环引用导致的无限递归

DataView 和 ArrayBuffer 比较

javascript 复制代码
case dataViewTag:
  if ((object.byteLength != other.byteLength) ||
      (object.byteOffset != other.byteOffset)) {
    return false;
  }
  object = object.buffer;
  other = other.buffer;

case arrayBufferTag:
  if ((object.byteLength != other.byteLength) ||
      !equalFunc(new Uint8Array(object), new Uint8Array(other))) {
    return false;
  }
  return true;

对于 DataView 和 ArrayBuffer 类型:

  1. 对于 DataView,首先比较 byteLengthbyteOffset,如果不同则返回 false
  2. 然后将 DataView 的比较转换为对其底层 ArrayBuffer 的比较(注意这里没有 break,是故意的)
  3. 对于 ArrayBuffer,比较 byteLength,然后使用 Uint8Array 视图逐字节比较内容
  4. 如果所有检查都通过,返回 true
DataView 介绍

DataView 是一个用于读写 ArrayBuffer 内容的底层接口,它提供了对二进制数据的不同类型(如 Int8、Uint8、Int16、Uint16 等)的读写能力。DataView 的主要特点是:

  1. 字节序控制:可以控制数据的字节序(大端序或小端序)
  2. 类型安全:提供了多种数据类型的读写方法
  3. 偏移量访问:可以指定从哪个字节开始读写数据
为什么 DataView 和 ArrayBuffer 放在一起处理?

DataView 和 ArrayBuffer 放在一起处理的原因是:

  1. 数据关系:DataView 是建立在 ArrayBuffer 之上的视图,每个 DataView 实例都有一个底层的 ArrayBuffer
  2. 比较策略:DataView 的比较最终可以归结为对其底层 ArrayBuffer 的比较
  3. 代码复用:通过将 DataView 转换为 ArrayBuffer,可以复用 ArrayBuffer 的比较逻辑
  4. 性能优化:避免重复实现类似的比较逻辑
为什么使用 Uint8Array?

使用 Uint8Array 而不是其他类型数组的原因:

  1. 最基础的字节表示

    • Uint8Array 每个元素表示一个字节(8 位无符号整数)
    • 范围是 0-255,正好对应一个字节的所有可能值
    • 其他类型(如 Int16Array、Float32Array 等)会涉及多个字节的表示
  2. 字节序无关

    • Uint8Array 是字节序无关的,每个元素就是一个字节
    • 其他类型数组需要考虑字节序问题(大端序/小端序)
  3. 内存效率

    • Uint8Array 是最紧凑的表示方式,每个元素只占用一个字节
    • 其他类型数组会占用更多内存,且可能包含填充字节
  4. 兼容性

    • Uint8Array 是所有 TypedArray 中最基础和广泛支持的
    • 在所有现代浏览器中都有良好的支持

示例说明:

javascript 复制代码
// 创建一个包含 4 个字节的 ArrayBuffer
const buffer = new ArrayBuffer(4);
const view = new DataView(buffer);

// 写入一些数据
view.setInt16(0, 42, true); // 小端序写入 42
view.setInt16(2, 24, true); // 小端序写入 24

// 使用 Uint8Array 查看原始字节
const bytes = new Uint8Array(buffer);
console.log(bytes); // [42, 0, 24, 0]

// 使用 Int16Array 查看(需要考虑字节序)
const ints = new Int16Array(buffer);
console.log(ints); // [42, 24]

基本类型比较

javascript 复制代码
case boolTag:
case dateTag:
case numberTag:
  // Coerce booleans to `1` or `0` and dates to milliseconds.
  // Invalid dates are coerced to `NaN`.
  return eq(+object, +other);

对于布尔值、日期和数字类型:

  1. 使用一元加操作符 + 将它们转换为数字
    • 布尔值:true -> 1, false -> 0
    • 日期:转换为毫秒时间戳
    • 数字:保持不变
    • 无效日期:转换为 NaN
  2. 使用 eq 函数比较转换后的值,它能正确处理 NaN 的比较

Error 对象比较

javascript 复制代码
case errorTag:
  return object.name == other.name && object.message == other.message;

对于 Error 对象,比较它们的 namemessage 属性是否相同。

RegExp 和 String 比较

javascript 复制代码
case regexpTag:
case stringTag:
  // Coerce regexes to strings and treat strings, primitives and objects,
  // as equal. See http://www.ecma-international.org/ecma-262/7.0/#sec-regexp.prototype.tostring
  // for more details.
  return object == (other + '');

对于正则表达式和字符串:

  1. other 转换为字符串(通过 + '' 操作)
  2. 使用宽松相等(==)比较,这样可以处理字符串对象和字符串原始值的比较

Map 和 Set 比较

javascript 复制代码
case mapTag:
  var convert = mapToArray;

case setTag:
  var isPartial = bitmask & COMPARE_PARTIAL_FLAG;
  convert || (convert = setToArray);

  if (object.size != other.size && !isPartial) {
    return false;
  }
  // Assume cyclic values are equal.
  var stacked = stack.get(object);
  if (stacked) {
    return stacked == other;
  }
  bitmask |= COMPARE_UNORDERED_FLAG;

  // Recursively compare objects (susceptible to call stack limits).
  stack.set(object, other);
  var result = equalArrays(convert(object), convert(other), bitmask, customizer, equalFunc, stack);
  stack['delete'](object);
  return result;

对于 Map 和 Set 类型:

  1. 根据类型选择适当的转换函数:
    • Map:使用 mapToArray 转换为键值对数组 [[key1, value1], [key2, value2], ...]
    • Set:使用 setToArray 转换为值数组 [value1, value2, ...]
  2. 检查大小是否相同(在部分比较模式下可以忽略)
  3. 处理循环引用:
    • 检查对象是否已经在比较栈中
    • 如果是,则假定循环引用的值相等,直接比较栈中的引用
  4. 设置无序比较标志(COMPARE_UNORDERED_FLAG),因为 Map 和 Set 的迭代顺序可能不同
  5. 将对象添加到比较栈中
  6. 将 Map 或 Set 转换为数组,然后使用 equalArrays 递归比较
  7. 从比较栈中移除对象
  8. 返回比较结果

Symbol 比较

javascript 复制代码
case symbolTag:
  if (symbolValueOf) {
    return symbolValueOf.call(object) == symbolValueOf.call(other);
  }

对于 Symbol 类型:

  1. 检查 symbolValueOf 是否可用(Symbol.prototype.valueOf 的引用)
  2. 如果可用,调用对象的 valueOf 方法获取原始值,然后比较

默认情况

javascript 复制代码
}
return false;

如果没有匹配的类型或比较失败,返回 false

与其他比较函数的关系

equalByTag 是 Lodash 深度相等比较系统中的一个重要组件,它与其他几个比较函数一起工作:

  1. baseIsEqual :最顶层的比较函数,处理基本情况并调用 baseIsEqualDeep

    javascript 复制代码
    function baseIsEqual(value, other, bitmask, customizer, stack) {
      if (value === other) {
        return true;
      }
      if (
        value == null ||
        other == null ||
        (!isObjectLike(value) && !isObjectLike(other))
      ) {
        return value !== value && other !== other;
      }
      return baseIsEqualDeep(
        value,
        other,
        bitmask,
        customizer,
        baseIsEqual,
        stack
      );
    }
  2. baseIsEqualDeep:处理深度比较的主要函数,根据对象类型分发到不同的比较函数

    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 && !objIsObj) {
        return objIsArr
          ? equalArrays(object, other, bitmask, customizer, equalFunc, stack)
          : equalByTag(
              object,
              other,
              objTag,
              bitmask,
              customizer,
              equalFunc,
              stack
            );
      }
    
      // ... 其他代码 ...
    }
  3. equalArrays:专门比较数组的函数,支持有序和无序比较

    javascript 复制代码
    function equalArrays(array, other, bitmask, customizer, equalFunc, stack) {
      // ... 数组比较逻辑 ...
    }
  4. equalObjects:专门比较普通对象的函数

    javascript 复制代码
    function equalObjects(object, other, bitmask, customizer, equalFunc, stack) {
      // ... 对象比较逻辑 ...
    }

这些函数共同构成了一个强大的深度比较系统,能够处理各种 JavaScript 数据类型和复杂的嵌套结构。

总结

equalByTag 是 Lodash 中一个专门用于比较具有相同类型标签的对象的内部工具函数。它通过针对不同类型实现特定的比较逻辑,解决了 JavaScript 中深度比较的复杂问题。

这个函数的主要特点包括:

  1. 类型特化:为不同的 JavaScript 内置类型提供专门的比较逻辑
  2. 灵活性:支持部分比较和无序比较等高级功能
  3. 健壮性 :能够处理循环引用和特殊值(如 NaN
  4. 高效性:通过类型转换和短路逻辑优化比较性能
  5. 可扩展性:通过自定义比较器支持用户定制的比较逻辑

理解 equalByTag 的工作原理,有助于我们更好地使用 Lodash 的 _.isEqual 方法,并在自己的代码中实现更高效、更可靠的深度比较逻辑。在处理复杂数据结构、API 响应、配置对象等场景中,这种深度比较能力尤为重要。

相关推荐
加班是不可能的,除非双倍日工资2 小时前
css预编译器实现星空背景图
前端·css·vue3
wyiyiyi3 小时前
【Web后端】Django、flask及其场景——以构建系统原型为例
前端·数据库·后端·python·django·flask
gnip3 小时前
vite和webpack打包结构控制
前端·javascript
excel4 小时前
在二维 Canvas 中模拟三角形绕 X、Y 轴旋转
前端
阿华的代码王国4 小时前
【Android】RecyclerView复用CheckBox的异常状态
android·xml·java·前端·后端
一条上岸小咸鱼4 小时前
Kotlin 基本数据类型(三):Booleans、Characters
android·前端·kotlin
Jimmy4 小时前
AI 代理是什么,其有助于我们实现更智能编程
前端·后端·ai编程
ZXT4 小时前
promise & async await总结
前端
Jerry说前后端4 小时前
RecyclerView 性能优化:从原理到实践的深度优化方案
android·前端·性能优化
画个太阳作晴天4 小时前
A12预装app
linux·服务器·前端