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 响应、配置对象等场景中,这种深度比较能力尤为重要。

相关推荐
掘金安东尼30 分钟前
上周前端发生哪些新鲜事儿? #406
前端·面试·github
好_快1 小时前
Lodash源码阅读-getSymbols
前端·javascript·源码阅读
好_快1 小时前
Lodash源码阅读-keys
前端·javascript·源码阅读
亿牛云爬虫专家1 小时前
Headless Chrome 优化:减少内存占用与提速技巧
前端·chrome·内存·爬虫代理·代理ip·headless·大规模数据采集
好_快1 小时前
Lodash源码阅读-arrayFilter
前端·javascript·源码阅读
若云止水7 小时前
ngx_conf_handler - root html
服务器·前端·算法
佚明zj8 小时前
【C++】内存模型分析
开发语言·前端·javascript
知否技术9 小时前
ES6 都用 3 年了,2024 新特性你敢不看?
前端·javascript
最初@10 小时前
el-table + el-pagination 前端实现分页操作
前端·javascript·vue.js·ajax·html
大莲芒10 小时前
react 15-16-17-18各版本的核心区别、底层原理及演进逻辑的深度解析
javascript·react.js·ecmascript