Lodash 源码阅读-equalByTag
功能概述
equalByTag
是 Lodash 库中的一个内部工具函数,专门用于比较具有相同 toStringTag
的对象是否相等。它是深度相等比较(_.isEqual
)的核心组件之一,负责处理特定类型对象的比较逻辑。这个函数能够处理多种 JavaScript 内置类型,包括 Boolean
、Date
、Error
、Number
、RegExp
、String
、Map
、Set
、ArrayBuffer
、DataView
和 Symbol
等。
前置学习
依赖函数
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
(即对象的内部类型标识)选择适当的比较策略:
- 类型分发 :使用
switch
语句根据对象的tag
选择相应的比较逻辑 - 特殊处理:对不同类型的对象采用不同的比较方法
- 类型转换:在比较前对某些类型进行适当的转换,以简化比较逻辑
- 递归比较:对于复杂类型如 Map 和 Set,将它们转换为数组后递归比较
- 循环引用处理 :使用
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 类型:
- 对于 DataView,首先比较
byteLength
和byteOffset
,如果不同则返回false
- 然后将 DataView 的比较转换为对其底层 ArrayBuffer 的比较(注意这里没有
break
,是故意的) - 对于 ArrayBuffer,比较
byteLength
,然后使用Uint8Array
视图逐字节比较内容 - 如果所有检查都通过,返回
true
DataView 介绍
DataView 是一个用于读写 ArrayBuffer 内容的底层接口,它提供了对二进制数据的不同类型(如 Int8、Uint8、Int16、Uint16 等)的读写能力。DataView 的主要特点是:
- 字节序控制:可以控制数据的字节序(大端序或小端序)
- 类型安全:提供了多种数据类型的读写方法
- 偏移量访问:可以指定从哪个字节开始读写数据
为什么 DataView 和 ArrayBuffer 放在一起处理?
DataView 和 ArrayBuffer 放在一起处理的原因是:
- 数据关系:DataView 是建立在 ArrayBuffer 之上的视图,每个 DataView 实例都有一个底层的 ArrayBuffer
- 比较策略:DataView 的比较最终可以归结为对其底层 ArrayBuffer 的比较
- 代码复用:通过将 DataView 转换为 ArrayBuffer,可以复用 ArrayBuffer 的比较逻辑
- 性能优化:避免重复实现类似的比较逻辑
为什么使用 Uint8Array?
使用 Uint8Array 而不是其他类型数组的原因:
-
最基础的字节表示:
- Uint8Array 每个元素表示一个字节(8 位无符号整数)
- 范围是 0-255,正好对应一个字节的所有可能值
- 其他类型(如 Int16Array、Float32Array 等)会涉及多个字节的表示
-
字节序无关:
- Uint8Array 是字节序无关的,每个元素就是一个字节
- 其他类型数组需要考虑字节序问题(大端序/小端序)
-
内存效率:
- Uint8Array 是最紧凑的表示方式,每个元素只占用一个字节
- 其他类型数组会占用更多内存,且可能包含填充字节
-
兼容性:
- 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);
对于布尔值、日期和数字类型:
- 使用一元加操作符
+
将它们转换为数字- 布尔值:
true
->1
,false
->0
- 日期:转换为毫秒时间戳
- 数字:保持不变
- 无效日期:转换为
NaN
- 布尔值:
- 使用
eq
函数比较转换后的值,它能正确处理NaN
的比较
Error 对象比较
javascript
case errorTag:
return object.name == other.name && object.message == other.message;
对于 Error 对象,比较它们的 name
和 message
属性是否相同。
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 + '');
对于正则表达式和字符串:
- 将
other
转换为字符串(通过+ ''
操作) - 使用宽松相等(
==
)比较,这样可以处理字符串对象和字符串原始值的比较
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 类型:
- 根据类型选择适当的转换函数:
- Map:使用
mapToArray
转换为键值对数组[[key1, value1], [key2, value2], ...]
- Set:使用
setToArray
转换为值数组[value1, value2, ...]
- Map:使用
- 检查大小是否相同(在部分比较模式下可以忽略)
- 处理循环引用:
- 检查对象是否已经在比较栈中
- 如果是,则假定循环引用的值相等,直接比较栈中的引用
- 设置无序比较标志(
COMPARE_UNORDERED_FLAG
),因为 Map 和 Set 的迭代顺序可能不同 - 将对象添加到比较栈中
- 将 Map 或 Set 转换为数组,然后使用
equalArrays
递归比较 - 从比较栈中移除对象
- 返回比较结果
Symbol 比较
javascript
case symbolTag:
if (symbolValueOf) {
return symbolValueOf.call(object) == symbolValueOf.call(other);
}
对于 Symbol 类型:
- 检查
symbolValueOf
是否可用(Symbol.prototype.valueOf 的引用) - 如果可用,调用对象的
valueOf
方法获取原始值,然后比较
默认情况
javascript
}
return false;
如果没有匹配的类型或比较失败,返回 false
。
与其他比较函数的关系
equalByTag
是 Lodash 深度相等比较系统中的一个重要组件,它与其他几个比较函数一起工作:
-
baseIsEqual :最顶层的比较函数,处理基本情况并调用
baseIsEqualDeep
javascriptfunction 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 ); }
-
baseIsEqualDeep:处理深度比较的主要函数,根据对象类型分发到不同的比较函数
javascriptfunction 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 ); } // ... 其他代码 ... }
-
equalArrays:专门比较数组的函数,支持有序和无序比较
javascriptfunction equalArrays(array, other, bitmask, customizer, equalFunc, stack) { // ... 数组比较逻辑 ... }
-
equalObjects:专门比较普通对象的函数
javascriptfunction equalObjects(object, other, bitmask, customizer, equalFunc, stack) { // ... 对象比较逻辑 ... }
这些函数共同构成了一个强大的深度比较系统,能够处理各种 JavaScript 数据类型和复杂的嵌套结构。
总结
equalByTag
是 Lodash 中一个专门用于比较具有相同类型标签的对象的内部工具函数。它通过针对不同类型实现特定的比较逻辑,解决了 JavaScript 中深度比较的复杂问题。
这个函数的主要特点包括:
- 类型特化:为不同的 JavaScript 内置类型提供专门的比较逻辑
- 灵活性:支持部分比较和无序比较等高级功能
- 健壮性 :能够处理循环引用和特殊值(如
NaN
) - 高效性:通过类型转换和短路逻辑优化比较性能
- 可扩展性:通过自定义比较器支持用户定制的比较逻辑
理解 equalByTag
的工作原理,有助于我们更好地使用 Lodash 的 _.isEqual
方法,并在自己的代码中实现更高效、更可靠的深度比较逻辑。在处理复杂数据结构、API 响应、配置对象等场景中,这种深度比较能力尤为重要。