Lodash isEqual 方法源码实现分析
Lodash isEqual 方法源码实现分析
Lodash 的 isEqual
方法用于执行两个值的深度比较,以确定它们是否相等。这个方法能够处理各种 JavaScript 数据类型,包括基本类型、对象、数组、正则表达式、日期对象等,并且能够正确处理循环引用。
1. isEqual
函数入口
isEqual
函数本身非常简洁,它直接调用了内部的 baseIsEqual
函数:
// lodash.js L11599
function isEqual(value, other) {return baseIsEqual(value, other);
}
这表明核心的比较逻辑封装在 baseIsEqual
及其调用的更深层次的函数中。
2. baseIsEqual
函数
baseIsEqual
是执行比较的第一个主要关卡。它处理了一些基本情况,并将更复杂的对象和数组的比较委托给 baseIsEqualDeep
。
// lodash.js L3309
function baseIsEqual(value, other, bitmask, customizer, stack) {// 严格相等检查 (===),处理基本类型和相同对象的引用if (value === other) {return true;}// 处理 null 和 undefined,以及非对象类型的值(且不严格相等的情况)// 如果 value 或 other 为 null/undefined,或者两者都不是类对象 (object-like),// 则只有当它们都是 NaN 时才相等 (value !== value && other !== other 用于判断 NaN)if (value == null || other == null || (!isObjectLike(value) && !isObjectLike(other))) {return value !== value && other !== other;}// 对于对象和数组等复杂类型,调用 baseIsEqualDeep 进行深度比较return baseIsEqualDeep(value, other, bitmask, customizer, baseIsEqual, stack);
}
关键点:
- 严格相等:首先通过
===
检查两个值是否严格相等。如果它们是相同的基本类型值或指向同一个对象,则直接返回true
。 - Null/Undefined 和非对象处理:如果任一值为
null
或undefined
,或者两者都不是“类对象”(通过isObjectLike
判断,通常意味着它们不是对象或函数),则只有当两个值都是NaN
时才认为它们相等。value !== value
是判断一个值是否为NaN
的标准方法。 - 深度比较委托:对于其他情况(通常是两个都是类对象的情况),它将比较任务委托给
baseIsEqualDeep
函数。bitmask
、customizer
和stack
是用于支持更高级比较特性(如部分比较、自定义比较逻辑和循环引用处理)的参数。
接下来,我们将深入分析 baseIsEqualDeep
的实现。
3. baseIsEqualDeep
函数:核心深度比较逻辑
baseIsEqualDeep
是 isEqual
实现的核心,负责处理对象和数组的深度比较,并能够处理循环引用。
// lodash.js L3333
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);// 将 arguments 对象的标签视为普通对象标签objTag = objTag == argsTag ? objectTag : objTag;othTag = othTag == argsTag ? objectTag : othTag;var objIsObj = objTag == objectTag,othIsObj = othTag == objectTag,isSameTag = objTag == othTag;// 特殊处理 Buffer 类型:如果类型相同且一个是 Buffer,另一个也必须是 Buffer// 如果都是 Buffer,则后续按数组方式比较 (objIsArr = true, objIsObj = false)if (isSameTag && isBuffer(object)) {if (!isBuffer(other)) {return false;}objIsArr = true;objIsObj = false;}// 如果类型相同但不是普通对象 (e.g., Array, Date, RegExp, TypedArray)if (isSameTag && !objIsObj) {// 初始化用于循环引用检测的 stackstack || (stack = new Stack);// 如果是数组或 TypedArray,则调用 equalArrays 进行比较// 否则,调用 equalByTag 根据具体的标签类型进行比较 (e.g., RegExp, Date)return (objIsArr || isTypedArray(object))? equalArrays(object, other, bitmask, customizer, equalFunc, stack): equalByTag(object, other, objTag, bitmask, customizer, equalFunc, stack);}// 处理 Lodash 包装对象:如果不是部分比较 (partial comparison),// 且任一对象是 Lodash 包装对象 (具有 '__wrapped__' 属性),// 则解包后再进行比较。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);// 使用 equalFunc (即 baseIsEqual) 递归比较解包后的值return equalFunc(objUnwrapped, othUnwrapped, bitmask, customizer, stack);}}// 如果到这里,两个值的类型标签不同,则它们不相等if (!isSameTag) {return false;}// 初始化用于循环引用检测的 stackstack || (stack = new Stack);// 对于普通对象,调用 equalObjects 进行比较return equalObjects(object, other, bitmask, customizer, equalFunc, stack);
}
关键点:
- 类型识别:
- 使用
isArray
判断是否为数组。 - 使用
getTag
获取对象的内部[[ToString]]
标签 (如[object Array]
,[object Object]
,[object Date]
,[object RegExp]
) 来判断具体类型。 arguments
对象被特殊处理,其标签被视为[object Object]
。
- 使用
- Buffer 处理:如果两个值类型相同且其中一个是
Buffer
,那么另一个也必须是Buffer
才能继续比较。之后,Buffer
会被当作数组(objIsArr = true
)进行元素比较。 - 分支委托:
- 非普通对象且类型相同 (
isSameTag && !objIsObj
):- 如果是数组 (
objIsArr
) 或类型化数组 (isTypedArray(object)
),则调用equalArrays
进行比较。 - 否则(如
Date
,RegExp
,Map
,Set
等),调用equalByTag
进行特定类型的比较。
- 如果是数组 (
- Lodash 包装对象:如果对象是 Lodash 的包装对象 (e.g.,
_([1, 2])
) 并且当前不是部分比较模式,会先获取其原始值 (.value()
),然后再进行递归比较。 - 类型不同 (
!isSameTag
):如果此时发现两个值的类型标签不同,直接返回false
。 - 普通对象:如果以上条件都不满足,并且类型标签相同(通常意味着它们都是普通对象
[object Object]
),则调用equalObjects
进行对象属性的递归比较。
- 非普通对象且类型相同 (
- 循环引用处理 (
Stack
):在进行数组或对象的递归比较之前,会确保stack
对象已初始化 (stack || (stack = new Stack)
)。这个Stack
对象用于跟踪已经比较过的对象对,以防止因循环引用导致的无限递归。equalArrays
,equalByTag
, 和equalObjects
内部会使用这个stack
。 bitmask
和customizer
:这些参数会一路传递下去,供equalArrays
,equalByTag
,equalObjects
以及自定义比较函数使用,以支持部分比较、无序比较和用户自定义的比较逻辑。equalFunc
:这个参数通常是baseIsEqual
自身,用于在需要递归调用时返回到顶层的比较逻辑。
接下来,我们将分析 equalArrays
, equalByTag
, 和 equalObjects
这三个核心的比较辅助函数,以及 Stack
是如何工作的。
3.1. equalArrays
函数:比较数组和类数组对象
equalArrays
负责比较两个数组(或类数组对象,如 arguments
对象、TypedArray、Buffer)的内容是否相等。
// lodash.js L5675
function equalArrays(array, other, bitmask, customizer, equalFunc, stack) {var isPartial = bitmask & COMPARE_PARTIAL_FLAG, // 是否为部分比较arrLength = array.length,othLength = other.length;// 长度检查:如果长度不同,并且不是“部分比较且 other 长度大于 array 长度”的情况,则不等if (arrLength != othLength && !(isPartial && othLength > arrLength)) {return false;}// 循环引用检查:从 stack 中获取之前存储的 array 和 other// 如果两者都已存在于 stack 中,说明遇到了循环引用。// 此时,当且仅当 array 在 stack 中对应 other,并且 other 在 stack 中对应 array 时,才认为它们(在循环的这一点上)是相等的。var arrStacked = stack.get(array);var othStacked = stack.get(other);if (arrStacked && othStacked) {return arrStacked == other && othStacked == array;}var index = -1,result = true,// 如果是无序比较 (COMPARE_UNORDERED_FLAG),则创建一个 SetCache 用于跟踪 other 数组中已匹配的元素seen = (bitmask & COMPARE_UNORDERED_FLAG) ? new SetCache : undefined;// 将当前比较的 array 和 other 存入 stack,用于后续的循环引用检测stack.set(array, other);stack.set(other, array);// 遍历 array 的元素 (忽略非索引属性)while (++index < arrLength) {var arrValue = array[index],othValue = other[index]; // 在有序比较时,直接取 other 对应索引的值if (customizer) {// 如果提供了 customizer 函数,则调用它进行比较// 注意 partial 模式下 customizer 的参数顺序var compared = isPartial? customizer(othValue, arrValue, index, other, array, stack): customizer(arrValue, othValue, index, array, other, stack);}if (compared !== undefined) {// 如果 customizer 返回了明确的结果 (非 undefined)if (compared) {continue; // customizer认为相等,继续下一个元素}result = false; // customizer认为不等,数组不等,跳出循环break;}// 如果没有 customizer 或 customizer 返回 undefined,则进行标准比较if (seen) {// 无序比较 (COMPARE_UNORDERED_FLAG is set):// 尝试在 `other` 数组中找到一个与 `arrValue` 相等的元素,// 并且这个元素在 `other` 中的索引尚未被 `seen` (SetCache) 记录过。// `arraySome` 会遍历 `other` 数组。if (!arraySome(other, function(othElementValue, othElementIndex) {// 检查 othElementIndex 是否已在 seen 中,以及 arrValue 是否与 othElementValue 相等// 相等判断会优先使用 ===,然后递归调用 equalFunc (即 baseIsEqual)if (!cacheHas(seen, othElementIndex) &&(arrValue === othElementValue || equalFunc(arrValue, othElementValue, bitmask, customizer, stack))) {// 如果找到匹配且未被记录的元素,将其索引加入 seen 并返回 true (表示 arraySome 应该停止并返回 true)return seen.push(othElementIndex);}})) {// 如果 arraySome 返回 false (即在 other 中找不到匹配 arrValue 的元素),则数组不等result = false;break;}} else if (!(// 有序比较 (默认情况):// 比较当前索引的 arrValue 和 othValue 是否相等// 优先使用 ===,然后递归调用 equalFunc (即 baseIsEqual)arrValue === othValue ||equalFunc(arrValue, othValue, bitmask, customizer, stack))) {result = false; // 如果当前元素不等,则数组不等,跳出循环break;}}// 清理 stack 中为当前 array 和 other 设置的记录stack['delete'](array);stack['delete'](other);return result;
}
关键点:
- 长度检查:首先比较数组长度。只有在部分比较模式 (
isPartial
) 且other
数组长度大于array
数组长度时,不同长度才可能被接受。否则,长度不同直接返回false
。 - 循环引用处理:
- 在比较元素之前,通过
stack.get(array)
和stack.get(other)
检查这两个数组是否已经作为一对出现在比较栈中。如果是,说明遇到了循环引用。此时,只有当它们在栈中互相指向对方时,才认为这对循环引用是相等的(例如a = []; b = []; a.push(b); b.push(a); isEqual(a,b)
)。 - 在开始元素比较前,将
array
和other
互相注册到stack
中:stack.set(array, other)
和stack.set(other, array)
。 - 比较完成后,从
stack
中删除这对记录:stack['delete'](array)
和stack['delete'](other)
。
- 在比较元素之前,通过
- 元素比较:
- Customizer 优先:如果提供了
customizer
函数,则首先使用它来比较元素。如果customizer
返回一个布尔值,则该值决定了当前元素的比较结果。 - 有序比较 (默认):如果没有
customizer
或customizer
返回undefined
,并且不是无序比较模式,则按索引逐个比较元素。比较时先用===
,如果不等,则递归调用equalFunc
(即baseIsEqual
) 进行深度比较。 - 无序比较 (
COMPARE_UNORDERED_FLAG
):如果设置了无序比较标志,对于array
中的每个元素arrValue
,它会尝试在other
数组中找到一个与之相等的元素。这里使用了SetCache
(seen
) 来确保other
数组中的每个元素只被匹配一次。如果array
中的任何元素在other
中找不到未被匹配过的相等元素,则认为数组不等。
- Customizer 优先:如果提供了
bitmask
的作用:COMPARE_PARTIAL_FLAG
:影响长度检查和customizer
的调用方式。COMPARE_UNORDERED_FLAG
:切换到无序比较逻辑。
接下来分析 equalByTag
,它用于处理除了普通对象和数组之外的其他特定类型的对象比较。
3.2. equalByTag
函数:比较特定类型的对象
equalByTag
用于处理 baseIsEqualDeep
中那些类型相同但非普通对象(也不是数组或 TypedArray)的情况。它根据对象的 [[ToString]]
标签(tag
参数)来执行特定于类型的比较。
// lodash.js L5754
function equalByTag(object, other, tag, bitmask, customizer, equalFunc, stack) {switch (tag) {case dataViewTag: // For DataView// 比较 byteLength 和 byteOffsetif ((object.byteLength != other.byteLength) ||(object.byteOffset != other.byteOffset)) {return false;}// 如果上述相同,则将其内部的 ArrayBuffer 提取出来进行比较object = object.buffer;other = other.buffer;// 注意:这里没有 break,会继续执行 arrayBufferTag 的逻辑case arrayBufferTag: // For ArrayBuffer (and DataView's buffer)// 比较 byteLength// 然后将 ArrayBuffer 转换为 Uint8Array 再进行比较 (使用 equalFunc,通常是 baseIsEqual,最终会调用 equalArrays)if ((object.byteLength != other.byteLength) ||!equalFunc(new Uint8Array(object), new Uint8Array(other))) {return false;}return true;case boolTag: // For Boolean objects (e.g., new Boolean(true))case dateTag: // For Date objectscase numberTag: // For Number objects (e.g., new Number(1))// 将布尔值转换为 1 或 0,日期对象转换为毫秒数时间戳。// 无效日期会被转换为 NaN。// 使用宽松相等 `==` 来比较转换后的原始值 (e.g., +new Date() == +new Date())// `eq` 内部也是 `val1 === val2 || (val1 !== val1 && val2 !== val2)`,所以能正确处理 NaNreturn eq(+object, +other);case errorTag: // For Error objects// 比较 error 的 name 和 message 属性return object.name == other.name && object.message == other.message;case regexpTag: // For RegExp objectscase stringTag: // For String objects (e.g., new String('foo'))// 将正则表达式和字符串对象都转换为字符串原始值进行比较。// `other + ''` 是一种将 `other` 强制转换为字符串的方式。return object == (other + '');case mapTag: // For Map objectsvar convert = mapToArray; // mapToArray 将 Map 的键值对转换为 [key, value] 数组// 注意:这里没有 break,会继续执行 setTag 的逻辑case setTag: // For Set objectsvar isPartial = bitmask & COMPARE_PARTIAL_FLAG;convert || (convert = setToArray); // setToArray 将 Set 的值转换为数组// 比较大小 (size),除非是部分比较if (object.size != other.size && !isPartial) {return false;}// 处理循环引用:检查 object 是否已在 stack 中var stacked = stack.get(object);if (stacked) {// 如果在 stack 中,只有当它指向 other 时才认为相等return stacked == other;}// 对于 Map 和 Set,比较时总是认为是无序的 (COMPARE_UNORDERED_FLAG)bitmask |= COMPARE_UNORDERED_FLAG;// 将 Map/Set 转换为数组,然后使用 equalArrays 进行比较stack.set(object, other); // 存入 stack 以处理内部可能的循环引用var result = equalArrays(convert(object), convert(other), bitmask, customizer, equalFunc, stack);stack['delete'](object); // 从 stack 中移除return result;case symbolTag: // For Symbol objects// 如果支持 Symbol.prototype.valueOf (symbolValueOf),则比较它们的原始 Symbol 值if (symbolValueOf) {return symbolValueOf.call(object) == symbolValueOf.call(other);}// 如果不支持 (极旧环境),则无法可靠比较,返回 false}return false; // 对于未处理的标签类型,返回 false
}
关键点:
- Switch based on Tag:函数的核心是一个
switch
语句,根据传入的tag
(对象的[[ToString]]
标签)来执行不同的比较逻辑。 - DataView 和 ArrayBuffer:
DataView
:首先比较byteLength
和byteOffset
。如果相同,则取出其底层的ArrayBuffer
,然后逻辑会 fall-through 到arrayBufferTag
的处理。ArrayBuffer
:比较byteLength
。如果相同,则将两个ArrayBuffer
都包装成Uint8Array
,然后递归调用equalFunc
(即baseIsEqual
,最终会走到equalArrays
) 进行字节级别的比较。
- Boolean, Date, Number Objects:这些包装对象通过一元加号
+
被转换为它们的原始值(数字或NaN
),然后使用eq
函数进行比较。eq(a, b)
等价于a === b || (a !== a && b !== b)
,可以正确处理NaN
。 - Error Objects:比较
name
和message
属性是否相等。 - RegExp 和 String Objects:将它们都转换为字符串原始值,然后使用
==
进行比较。 - Map 和 Set:
- 首先检查
size
是否相等(除非是部分比较)。 - 处理循环引用:通过
stack.get(object)
检查当前Map
或Set
是否已在比较栈中。如果是,则只有当栈中记录的对应值是other
时才认为它们相等。 - 将
Map
或Set
的内容转换为数组(Map
转为[[key, value], ...]
数组,Set
转为[value, ...]
数组)。 - 在转换后的数组上调用
equalArrays
进行比较。此时,bitmask
会强制加入COMPARE_UNORDERED_FLAG
,因为Map
和Set
的元素顺序通常不重要(对于isEqual
而言,Lodash 将它们视为无序集合进行比较)。 - 在递归调用
equalArrays
前后,会通过stack.set
和stack.delete
管理当前Map
/Set
的循环引用跟踪。
- 首先检查
- Symbol Objects:如果环境支持
Symbol.prototype.valueOf
,则调用它获取原始Symbol
值进行比较。否则,认为它们不等。 - Fall-through:
dataViewTag
的处理会自然地落到arrayBufferTag
。mapTag
的处理会自然地落到setTag
的共享逻辑部分(主要是循环引用检查和转换为数组后调用equalArrays
的部分)。
接下来分析 equalObjects
,它用于比较普通对象的属性。
3.3. equalObjects
函数:比较普通对象
equalObjects
负责比较两个普通对象(plain objects or objects with [[ToString]]
tag of [object Object]
)的属性是否相等。
// lodash.js L5832
function equalObjects(object, other, bitmask, customizer, equalFunc, stack) {var isPartial = bitmask & COMPARE_PARTIAL_FLAG, // 是否为部分比较objProps = getAllKeys(object), // 获取 object 自身的可枚举属性名和 Symbol (包括原型链上的吗?getAllKeys 通常是自身的)objLength = objProps.length,othProps = getAllKeys(other), // 获取 other 自身的可枚举属性名和 SymbolothLength = othProps.length;// 属性数量检查:如果属性数量不同,并且不是部分比较模式,则不等if (objLength != othLength && !isPartial) {return false;}var index = objLength;// 检查 object 的每个属性是否存在于 other 中// 在部分比较模式下,只需检查 object 的属性是否在 other 中 (key in other)// 在完全比较模式下,检查 other 是否具有 object 的自身属性 (hasOwnProperty)while (index--) {var key = objProps[index];if (!(isPartial ? key in other : hasOwnProperty.call(other, key))) {return false;}}// 循环引用检查:与 equalArrays 中的逻辑类似var objStacked = stack.get(object);var othStacked = stack.get(other);if (objStacked && othStacked) {return objStacked == other && othStacked == object;}var result = true;// 将当前比较的 object 和 other 存入 stackstack.set(object, other);stack.set(other, object);var skipCtor = isPartial; // 在部分比较模式下,跳过构造函数检查index = -1; // 重置 index 用于遍历 objProps// 遍历 object 的所有属性进行比较while (++index < objLength) {key = objProps[index];var objValue = object[key],othValue = other[key];if (customizer) {// 如果提供了 customizer,则调用它var compared = isPartial? customizer(othValue, objValue, key, other, object, stack): customizer(objValue, othValue, key, object, other, stack);}// 如果 customizer 未返回明确结果,或者没有 customizer// 则递归比较属性值:先用 ===,然后用 equalFunc (baseIsEqual)if (!(compared === undefined? (objValue === othValue || equalFunc(objValue, othValue, bitmask, customizer, stack)): compared)) {result = false; // 如果任何属性值不等,则对象不等break;}// 如果不是部分比较,并且当前键是 'constructor',则标记 skipCtor 为 true// 这是为了后续的构造函数检查,如果用户明确比较了 'constructor' 属性,则跳过默认的构造函数检查skipCtor || (skipCtor = key == 'constructor');}if (result && !skipCtor) {// 如果所有属性都相等,并且没有跳过构造函数检查 (即 'constructor' 属性未被用户自定义比较)var objCtor = object.constructor,othCtor = other.constructor;// 额外的构造函数检查:// 如果两个对象的构造函数不同,并且它们都不是 Object 的直接实例 (通过检查 'constructor' in object/other 判断,// 并且构造函数本身不是 Function 的实例,这部分逻辑有点复杂,主要是为了排除 Object.create(null) 或字面量对象的情况),// 则认为对象不等。这个检查主要针对自定义类的实例。if (objCtor != othCtor &&('constructor' in object && 'constructor' in other) &&!(typeof objCtor == 'function' && objCtor instanceof objCtor &&typeof othCtor == 'function' && othCtor instanceof othCtor)) {result = false;}}// 清理 stackstack['delete'](object);stack['delete'](other);return result;
}
getAllKeys
函数 (L5915) 通常会返回对象自身的可枚举属性名和 Symbol 属性。
关键点:
- 属性数量和存在性检查:
- 首先,如果不是部分比较 (
isPartial
),会检查两个对象的属性数量是否相同。如果不同,直接返回false
。 - 然后,遍历
object
的所有属性,检查这些属性是否存在于other
对象中。在完全比较模式下,使用hasOwnProperty
确保是other
自身的属性;在部分比较模式下,仅使用in
操作符(允许原型链上的属性)。如果object
的任何属性在other
中不存在(根据模式),则返回false
。
- 首先,如果不是部分比较 (
- 循环引用处理:与
equalArrays
中的机制相同,使用stack
来检测和处理循环引用。 - 属性值比较:
- 遍历
object
的所有属性。 - Customizer 优先:如果提供了
customizer
,则用它比较属性值。 - 递归比较:如果没有
customizer
或它返回undefined
,则先用===
比较属性值。如果它们不严格相等,则递归调用equalFunc
(即baseIsEqual
) 对属性值进行深度比较。
- 遍历
- 构造函数检查:
- 在所有属性值都相等后(
result
仍为true
),并且没有因为用户自定义比较constructor
属性而跳过此检查 (!skipCtor
),会进行一个额外的构造函数检查。 - 如果两个对象的
constructor
属性不同,并且这两个对象都有constructor
属性,且它们的构造函数不是简单的Object
(通过一个略显复杂的instanceof
检查来判断,主要是为了确保它们是自定义类的实例),那么这两个对象被认为是不等的。这个检查的目的是确保由不同类创建的实例,即使属性相同,也被视为不等。
- 在所有属性值都相等后(
getAllKeys
:这个辅助函数用于获取对象的所有自身属性键(包括字符串键和 Symbol 键)。
4. 循环引用处理:Stack
数据结构
Lodash 使用一个名为 Stack
的内部数据结构来跟踪在深度比较过程中遇到的对象对,以防止因循环引用导致的无限递归。
// lodash.js L2322 (Stack constructor and methods)
function Stack(entries) {var data = this.__data__ = new ListCache(entries); // 内部使用 ListCachethis.size = data.size;
}function stackClear() { /* ... */ }
function stackDelete(key) { /* ... */ }
function stackGet(key) { /* ... */ }
function stackHas(key) { /* ... */ }
function stackSet(key, value) { /* ... */ }Stack.prototype.clear = stackClear;
Stack.prototype['delete'] = stackDelete;
Stack.prototype.get = stackGet;
Stack.prototype.has = stackHas;
Stack.prototype.set = stackSet;
Stack
内部主要依赖 ListCache
(L2035) 和 MapCache
(L2167)。
ListCache
:一个简单的缓存,用于存储少量键值对。它内部使用一个数组,并通过线性搜索进行查找、添加和删除。适用于缓存大小较小的情况。MapCache
:当缓存的条目数量超过一个阈值 (LARGE_ARRAY_SIZE
,默认为 200) 时,ListCache
可能会被转换为MapCache
(如果可用)。MapCache
使用 JavaScript 内置的Map
对象(如果可用)或一个哈希表实现(Hash
L1937,assocIndexOf
L1870)来提供更高效的查找,适用于存储大量键值对。
工作机制:
- 当
baseIsEqualDeep
开始比较两个对象(或数组)objA
和objB
时,它会(如果stack
不存在则创建)一个Stack
实例。 - 在
equalArrays
或equalObjects
(以及equalByTag
中处理 Map/Set 的部分) 中:- 检查是否存在:首先调用
stack.get(objA)
和stack.get(objB)
。如果stack.get(objA)
返回objB
并且stack.get(objB)
返回objA
,这意味着objA
和objB
已经作为一对被比较过并且形成了循环。在这种情况下,它们被认为是相等的(因为它们在循环点上互相引用)。如果stack.get(objA)
返回了某个值但不是objB
(或者反过来),这通常意味着一个对象在不同的比较路径中与不同的对象配对,这可能表示不等(具体取决于实现细节,但 Lodash 的逻辑是如果它们在栈中互相指向,则视为相等)。 - 存入栈中:在递归比较其内部元素或属性之前,会调用
stack.set(objA, objB)
和stack.set(objB, objA)
。这标记了objA
和objB
正在被比较。 - 移出栈中:在对
objA
和objB
的比较完成后(无论结果是相等还是不等),会调用stack.delete(objA)
和stack.delete(objB)
将它们从栈中移除。这允许这些对象在其他非循环的比较路径中被重新比较。
- 检查是否存在:首先调用
这种机制确保了如果比较过程中再次遇到已经处于当前比较路径上的同一对对象,比较会终止并返回 true
(假设它们互相引用),从而避免了无限循环。
5. SetCache
数据结构 (用于无序数组比较)
在 equalArrays
中,当进行无序比较时 (bitmask & COMPARE_UNORDERED_FLAG
),会使用 SetCache
来跟踪 other
数组中哪些元素已经被匹配过。
// lodash.js L2271 (SetCache constructor and methods)
function SetCache(values) {var index = -1,length = values == null ? 0 : values.length;this.__data__ = new MapCache; // 内部使用 MapCachewhile (++index < length) {this.add(values[index]);}
}function setCacheAdd(value) {this.__data__.set(value, HASH_UNDEFINED); // 值为一个特殊的 HASH_UNDEFINED 标记return this;
}function setCacheHas(value) {return this.__data__.has(value);
}SetCache.prototype.add = SetCache.prototype.push = setCacheAdd;
SetCache.prototype.has = setCacheHas;
SetCache
内部使用MapCache
来存储值。它只关心键(即数组中的元素),值本身并不重要,所以使用了一个常量HASH_UNDEFINED
作为所有键的值。add(value)
(或push(value)
): 将值添加到缓存中。has(value)
: 检查值是否存在于缓存中。
在 equalArrays
的无序比较逻辑中,当 array
中的一个元素 arrValue
与 other
数组中的一个元素 othElementValue
匹配成功后,othElementIndex
(或 othElementValue
,取决于具体实现,Lodash v4 中是 othIndex
) 会被添加到 seen
(一个 SetCache
实例) 中。这确保了 other
数组中的同一个元素不会被用来匹配 array
中的多个元素。
6. bitmask
和 customizer
参数
这两个参数贯穿了 isEqual
, baseIsEqual
, baseIsEqualDeep
, equalArrays
, equalByTag
, 和 equalObjects
的调用链,用于提供更灵活的比较行为。
bitmask
:一个数字,其位用于表示不同的比较标志。COMPARE_PARTIAL_FLAG
(值为 1):启用“部分比较”模式。在此模式下:- 对于对象:
isEqual({ 'a': 1 }, { 'a': 1, 'b': 2 })
在部分比较下可能为true
(如果object
是第一个参数),因为它只检查第一个对象的属性是否存在于第二个对象中且值相等。 - 对于数组:长度比较会更宽松,
customizer
的参数顺序可能会调整。 isMatch
函数内部会使用这个标志。
- 对于对象:
COMPARE_UNORDERED_FLAG
(值为 2):启用“无序比较”模式,主要用于数组。在此模式下,数组元素的顺序不重要,只要所有元素都存在于另一个数组中(考虑数量)。Map
和Set
的比较内部也会强制使用此标志。
customizer
:一个可选的回调函数,用户可以提供它来自定义特定值对的比较逻辑。customizer
函数被调用时会接收参数如(objValue, othValue, keyOrIndex, object, other, stack)
。- 如果
customizer
返回true
,则认为这对值相等。 - 如果
customizer
返回false
,则认为这对值不等。 - 如果
customizer
返回undefined
,则isEqual
会回退到其默认的比较逻辑来处理这对值。 isEqualWith
函数就是isEqual
的一个版本,它明确接受一个customizer
参数。
7. 总结:isEqual
实现策略
Lodash 的 isEqual
方法采用了一个分层、递归的策略来实现深度比较:
- 入口与基本情况 (
isEqual
->baseIsEqual
):- 快速路径:通过
===
检查严格相等。 - 处理
null
,undefined
, 和非对象类型(包括NaN
)。
- 快速路径:通过
- 核心深度比较 (
baseIsEqualDeep
):- 类型检测:使用
isArray
,getTag
,isBuffer
,isTypedArray
等确定值的具体类型。 - 分支委托:
- 数组/TypedArray ->
equalArrays
- Buffer -> 特殊处理后转
equalArrays
- Date, RegExp, Boolean, Number, String, Error, Symbol 对象 ->
equalByTag
(进行特定于类型的原始值或属性比较) - Map, Set ->
equalByTag
(转换为数组后,使用equalArrays
进行无序比较) - 普通对象 ->
equalObjects
- Lodash 包装对象 -> 解包后递归调用
baseIsEqual
。
- 数组/TypedArray ->
- 类型检测:使用
- 递归比较与循环处理:
equalArrays
和equalObjects
(以及equalByTag
中的 Map/Set 逻辑) 会递归调用baseIsEqual
(通过equalFunc
参数) 来比较嵌套的元素或属性值。Stack
数据结构用于在递归过程中跟踪已比较的对象对,以正确处理循环引用并防止无限递归。
- 特定集合类型的处理:
- 数组 (
equalArrays
):支持有序比较(默认)和无序比较(通过COMPARE_UNORDERED_FLAG
和SetCache
)。 - 对象 (
equalObjects
):比较对象的属性数量和每个属性的值。包含一个特殊的构造函数检查。
- 数组 (
- 灵活性:
- 通过
bitmask
支持部分比较和无序比较等模式。 - 通过
customizer
函数允许用户提供自定义的比较逻辑。
- 通过
这种设计使得 isEqual
非常健壮,能够准确处理 JavaScript 中广泛的数据类型和复杂结构,同时通过 Stack
机制有效地解决了循环引用的问题。
相关文章:
Lodash isEqual 方法源码实现分析
Lodash isEqual 方法源码实现分析 Lodash 的 isEqual 方法用于执行两个值的深度比较,以确定它们是否相等。这个方法能够处理各种 JavaScript 数据类型,包括基本类型、对象、数组、正则表达式、日期对象等,并且能够正确处理循环引用。 1. is…...

Qt Widgets模块功能详细说明,基本控件:QCheckBox(三)
一、基本控件(Widgets) Qt 提供了丰富的基本控件,如按钮、标签、文本框、复选框、单选按钮、列表框、组合框、菜单、工具栏等。 1、QCheckBox 1.1、概述 (用途、状态、继承关系) QCheckBox 是 Qt 框架中的复选框控件,用于表示二…...

第四天的尝试
目录 一、每日一言 二、练习题 三、效果展示 四、下次题目 五、总结 一、每日一言 很抱歉的说一下,我昨天看白色巨塔电视剧,看的入迷了,同时也看出一些道理,学到东西; 但是把昨天的写事情给忘记了,今天…...
【git进阶】git rebase(变基)
git rebase有很多用武之地,我一一道来 合并分支 当多人协作同一个分支时,在提交我们自己版本之前,我们会先用git pull获取远端最新的版本。但是 git pull = git fetch + git mergegit merge是一个非线性的合并操作,大量的merge会造成日志线的分散和交错。实际上 git pu…...

WPS中代码段的识别方法及JS宏实现
在WPS中,文档的基本结构可以通过对象模型来理解: (1)Document对象:表示整个文档 (2)Range对象:表示文档中的一段连续区域,可以是一个字符、一个句子或整个文档 &#…...

小米MUJIA智能音频眼镜来袭
智能眼镜赛道风云再起,小米新力作MIJIA智能音频眼镜2正式亮相,引发市场热议。 这款产品在设计和功能上都有显著提升,为用户带来更舒适便捷的佩戴体验,同时也标志着小米在智能眼镜领域的持续深耕。 轻薄设计,舒适体验 …...
【神经网络与深度学习】GAN 生成对抗训练模型在实际训练中很容易判别器收敛,生成器发散
引言部分 在深度学习领域,生成对抗网络(GAN)是一种强大的数据生成方法,它通过生成器(G)和判别器(D)之间的博弈来不断优化模型。然而,在实际训练过程中,GAN 往…...

基于SpringBoot的家政预约系统
作者:计算机学姐 开发技术:SpringBoot、SSM、Vue、MySQL、JSP、ElementUI、Python、小程序等,“文末源码”。 专栏推荐:前后端分离项目源码、SpringBoot项目源码、Vue项目源码、SSM项目源码、微信小程序源码 精品专栏:…...
[服务器备份教程] Rclone实战:自动备份数据到阿里云OSS/腾讯云COS等对象存储
更多服务器知识,尽在hostol.com 各位服务器的守护者们,咱们都知道,数据是数字时代的“黄金”,而服务器上的数据更是我们业务的命脉。可天有不测风云,硬盘可能会突然“寿终正寝”,手滑执行了“毁灭性”命令…...
使用 Whisper 生成视频字幕:从提取音频到批量处理
生成视频字幕是许多视频处理任务的核心需求。本文将指导你使用 OpenAI 的 Whisper 模型为视频文件(如电视剧《Normal People》或电影《花样年华》)生成字幕(SRT 格式)。我们将从提取音频开始,逐步实现字幕生成…...

Axure难点解决分享:垂直菜单展开与收回(4大核心问题与专家级解决方案)
亲爱的小伙伴,在您浏览之前,烦请关注一下,在此深表感谢!如有帮助请订阅专栏! Axure产品经理精品视频课已登录CSDN可点击学习https://edu.csdn.net/course/detail/40420 课程主题:垂直菜单展开与收回 主要内容:超长菜单实现、展开与收回bug解释、Axure9版本限制等问题解…...

Linux:网络层的重要协议或技术
一、DNS DNS(Domain Name System)是一整套从域名映射到IP的系统 1.1 DNS的背景 TCP/IP中使用IP地址和端口号来确定网络上的一台主机的一个程序. 但是IP地址不方便记忆. 于是人们发明了一种叫主机名的东西, 是一个字符串, 并且使用hosts文件来描述主机名和IP地址的关系. 最初,…...
【Hadoop 实战】Yarn 模式上传 HDFS 卡顿时 “No Route to Host“ 错误深度解析与解决方案
🌟 飞哥带你攻克 Hadoop 网络通信难题 大家好,我是小飞!最近在大数据集群运维中遇到一个典型问题:使用 Yarn 模式向 HDFS 上传大文件时进度条卡住不动,查看日志发现关键报错: No Route to Host from BigDat…...

JAVA请求vllm的api服务报错Unsupported upgrade request、 Invalid HTTP request received.
环境: vllm 0.8.5 java 17 Qwen3-32B-FP8 问题描述: JAVA请求vllm的api服务报错Unsupported upgrade request、 Invalid HTTP request received. WARNING: Unsupported upgrade request. INFO: - "POST /v1/chat/completions HTTP/1.1&…...

基于 CSS Grid 的网页,拆解页面整体布局结构
通过以下示例拆解网页整体布局结构: 一、基础结构(HTML骨架) <!DOCTYPE html> <html lang"zh-CN"> <head><meta charset"UTF-8"><meta http-equiv"X-UA-Compatible" content"…...
华为云Astro轻应用创建业务对象(BO)的概念梳理
目录 一、业务对象(BO)是什么?——【详细概念解释】 二、形象理解业务对象(BO) 🍱 类比方式: 📦 举个具体例子:以做一个“智能烟雾报警系统”应用 三、为什么使用BO很重要? 四、小结: 一、业务对象(BO)是什么?——【详细概念解释】 在华为云Astro轻应用…...
利用systemd启动部署在服务器上的web应用
0.背景 系统环境: Ubuntu 22.04 web应用情况: 前后端分类,前端采用react,后端采用fastapi 1.具体配置 1.1 前端配置 开发态运行(启动命令是npm run dev),创建systemd服务文件 sudo nano /etc/systemd/…...
ArkUI Tab组件开发深度解析与应用指南
ArkUI Tab组件开发深度解析与应用指南 一、组件架构与核心能力 ArkUI的Tabs组件采用分层设计结构,由TabBar(导航栏)和TabContent(内容区)构成,支持底部、顶部、侧边三种导航布局模式。组件具备以下核心特…...

psotgresql18 源码编译安装
环境: 系统:centos7.9 数据库:postgresql18beta1 #PostgreSQL 18 已转向 DocBook XML 构建体系(SGML 未来将被弃用)。需要安装 XML 工具链,如下: yum install -y docbook5-style-xsl libxsl…...

虚幻引擎5-Unreal Engine笔记之Pawn与胶囊体的关系
虚幻引擎5-Unreal Engine笔记之Pawn与胶囊体的关系 code review! 文章目录 虚幻引擎5-Unreal Engine笔记之Pawn与胶囊体的关系1. 什么是Pawn?2. 什么是胶囊体(Capsule Component)?3. Pawn与胶囊体的具体关系(1&#x…...
python创建flask项目
好的,我会为你提供一个使用 Flask、pg8000 和 Pandas 构建的后台基本框架,用于手机理财产品 App 的报表分析接口。这个框架将包含异常处理、模块化的结构以支持多人协作,以及交易分析和收益分析的示例接口。 项目结构: financial_report_ap…...

Vue环境下数据导出PDF的全面指南
文章目录 1. 前言2. 原生浏览器打印方案2.1 使用window.print()实现2.2 使用CSS Paged Media模块 3. 常用第三方库方案3.1 使用jsPDF3.2 使用html2canvas jsPDF3.3 使用pdfmake3.4 使用vue-pdf 4. 服务器端导出方案4.1 前端请求服务器生成PDF4.2 使用无头浏览器生成PDF 5. 方法…...

Linux中的DNS的安装与配置
DNS简介 DNS(DomainNameSystem)是互联网上的一项服务,它作为将域名和IP地址相互映射的一个分布式数据库,能够使人更方便的访问互联网。 DNS使用的是53端口 通常DNS是以UDP这个较快速的数据传输协议来查询的,但是没有查…...

linux服务器与时间服务器同步时间
内网部署服务器,需要同步时间 使用系统内置的systemctl-timesyncd进行时间同步 1.编辑配置文件 sudo nano /etc/systemd/timesyncd.conf修改添加内容入下 [Time] NTP10.100.13.198 FallbackNTP#说明 #NTP10.100.13.198:你的主 NTP 时间服务器 IP #Fall…...

【数据结构篇】排序1(插入排序与选择排序)
注:本文以排升序为例 常见的排序算法: 目录: 一 直接插入排序: 1.1 基本思想: 1.2 代码: 1.3 复杂度: 二 希尔排序(直接插入排序的优化): 2.1 基本思想…...

《Linux服务与安全管理》| DNS服务器安装和配置
《Linux服务与安全管理》| DNS服务器安装和配置 目录 《Linux服务与安全管理》| DNS服务器安装和配置 第一步:使用dnf命令安装BIND服务 第二步:查看服务器server01的网络配置 第三步:配置全局配置文件 第四步:修改bind的区域…...
【NLP】34. 数据专题:如何打造高质量训练数据集
构建大语言模型的秘密武器:如何打造高质量训练数据集? 在大语言模型(LLM)如 GPT、BERT、T5 爆发式发展的背后,我们常常关注模型架构的演化,却忽视了一个更基础也更关键的问题:训练数据从哪里来…...

Notepad++ 学习(三)使用python插件编写脚本:实现跳转指定标签页(自主研发)
目录 一、先看成果二、安装Python Script插件三、配置Python脚本四、使用脚本跳转标签页方法一:通过菜单运行方法二:设置快捷键(推荐) 五、注意事项六、进阶使用 官网地址: https://notepad-plus-plus.org/Python Scri…...

Stable Diffusion 学习笔记02
模型下载网站: 1,LiblibAI-哩布哩布AI - 中国领先的AI创作平台 2,Civitai: The Home of Open-Source Generative AI 模型的安装: 将下载的sd模型放置在sd1.5的文件内即可,重启客户端可用。 外挂VAE模型:…...

python:pymysql概念、基本操作和注入问题讲解
python:pymysql分享目录 一、概念二、数据准备三、安装pymysql四、pymysql使用(一)使用步骤(二)查询操作(三)增(四)改(五)删 五、关于pymysql注入…...