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…...
探索边缘计算:赋能物联网的未来
摘要 随着物联网(IoT)技术的飞速发展,越来越多的设备接入网络,产生了海量的数据。传统的云计算模式在处理这些数据时面临着延迟高、带宽不足等问题,而边缘计算的出现为解决这些问题提供了新的思路。本文将深入探讨边缘…...
Ubuntu中配置【Rust 镜像源】
本篇主要记录Ubuntu中配置Rust编程环境时,所需要做的镜像源相关的配置 无法下载 Rust 工具链 通过环境变量指定 Rust 的国内镜像源(如中科大或清华源)。 方法一:临时设置镜像 export RUSTUP_DIST_SERVERhttps://mirrors.ustc.e…...
netty 客户端发送消息服务端收到消息无法打印,springBoot配合 lombok使用@Slf4j
netty 客户端发送消息服务端收到消息无法打印,springBoot配合 lombok使用Slf4j 服务端代码 Slf4j public class EventLoopServer {public static void main(String[] args) throws InterruptedException {new ServerBootstrap().group(new NioEventLoopGroup()).c…...
学习笔记:黑马程序员JavaWeb开发教程(2025.4.3)
12.1 基础登录功能 EmpService中的login方法,是根据接收到的用户名和密码,查询时emp数据库中的员工信息,会返回一个员工对象。使用了三元运算符来写返回 Login是登录,是一个业务方法,mapper接口是持久层,是…...
Spark SQL 运行架构详解(专业解释+番茄炒蛋例子解读)
1. 整体架构概览 Spark SQL的运行过程可以想象成一个"SQL查询的加工流水线",从原始SQL语句开始,经过多个阶段的处理和优化,最终变成分布式计算任务执行。主要流程如下: SQL Query → 解析 → 逻辑计划 → 优化 → 物理…...
【时时三省】(C语言基础)字符数组的输入输出
山不在高,有仙则名。水不在深,有龙则灵。 ----CSDN 时时三省 字符数组的输入输出可以有两种方法。 ( 1 )逐个字符输入输出。用格式符“% c”输入或输出一个字符. ( 2 )将整个字符串一次输入或输出。用“% s”格式符,意思是对字符串( strin…...
Hive HA配置高可用
Hive的高可用性(HA)通过消除关键组件的单点故障来实现,确保系统在部分故障时仍能正常运行。其基本原理涉及以下核心组件和策略: 1. Hive Metastore 的高可用 多实例部署:部署多个Metastore服务实例,每个实例连接到共享的后端数据库(如MySQL、PostgreSQ…...
Python爬虫第20节-使用 Selenium 爬取小米商城空调商品
目录 前言 一、 本文目标 二、环境准备 2.1 安装依赖 2.2 配置 ChromeDriver 三、小米商城页面结构分析 3.1 商品列表结构 3.2 分页结构 四、Selenium 自动化爬虫实现 4.1 脚本整体结构 4.2 代码实现 五、关键技术详解 5.1 Selenium 启动与配置 5.2 页面等待与异…...
重构金融数智化产业版图:中电金信“链主”之道
近日,《商学院》杂志独家专访了中电金信常务副总经理(主持经营工作)冯明刚,围绕“金融科技”“数字底座”“架构转型”“AI驱动”等议题,展开了一场关于未来架构、技术变革与系统创新的深入对话。 当下,数字…...
笔记本电脑升级实战手册【扩展篇1】:flash id查询硬盘颗粒
文章目录 前言:一、硬盘颗粒介绍1、MLC(Multi-Level Cell)2、TLC(Triple-Level Cell)3、QLC(Quad-Level Cell) 二、硬盘与主控1、主控介绍2、主流主控厂家 三 、硬盘颗粒查询使用flash id工具查…...
文档外发安全:企业数据防护的最后一道防线
在当今数字化时代,数据已成为企业最宝贵的资产之一。随着网络安全威胁日益增多,企业安装专业加密软件已从"可选"变为"必选"。本文将全面分析企业部署华途加密解决方案后获得的各项战略优势。 一、数据安全防护升级 核心数据全面保护…...
springboot集成langchain4j实现票务助手实战
前言 看此篇的前置知识为langchain4j整合springboot,以及springboot集成langchain4j记忆对话。 Function-Calls介绍 langchain4j 中的 Function Calls(函数调用)是一种让大语言模型(LLM)与外部工具(如 A…...
ZYNQ笔记(二十一): VDMA HDMI 彩条显示
版本:Vivado2020.2(Vitis) 任务:实现驱动 HDMI 显示彩条图像,同时支持输出给 HDMI 的图像分辨率可调。 目录 一、介绍 二、硬件设计 (1)DVI_Transmitter (2)Clockin…...
常用的maven插件及其使用指南
目录 1.maven官方插件列表2.两种方式调用maven插件3.常用的maven插件总结参考文献 1.maven官方插件列表 groupId为org.apache.maven.pluginshttp://maven.apache.org/plugins/index.html 2.两种方式调用maven插件 将插件目标与生命周期阶段绑定,例如maven默认将m…...
Meilisearch 安装
1.环境 rockey linux 9.2 meilisearch-linux-amd64 2.下载 访问:https://github.com/meilisearch/meilisearch/releases 下载适合自己系统版本的。 注意:我下载的不是最新版本的,因为最新版本的需要GLIBC2.35,我本地系统的是…...
用postman的时候如何区分服务器还是自己的问题?
作为测试人员,在使用Postman进行接口测试时,准确判断问题是出在服务器端还是本地环境非常重要。以下是一些实用的区分方法: 1. 基础检查方法 本地问题排查清单: ✅ 检查网络连接是否正常 ✅ 确认Postman版本是否为最新 ✅ 验证请求URL是否正确(特别是环境变量是否被正确…...
【Python算法】最长递增子序列
题目链接 方法1: 记忆化搜索 class Solution:def lengthOfLIS(self, nums: List[int]) -> int:cachedef dfs(i):res0 for j in range(i):if nums[j]<nums[i]:res max(res,dfs(j))return res1 # 返回res表示以nums[i]结尾的LIS长度return max(dfs(i) for i…...
springboot-web基础
21.web spring MVC 基于浏览器的 B/S 结构应用十分流行。Spring Boot 非常适合 Web 应用开发。可以使用嵌入式 Tomcat、Jetty、 Undertow 或 Netty 创建一个自包含的 HTTP 服务器。一个 Spring Boot 的 Web 应用能够自己独立运行,不依赖需 要安装的 Tomcat&#x…...
解构赋值
【系统学习ES6】 本专题旨在对ES6的常用技术点进行系统性梳理,帮助大家对其有更好的掌握,希望大家有所收获。 ES6允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构。解构是一种打破数据结构&#x…...
Leetcode-BFS问题
LeetCode-BFS问题 1.Floodfill问题 1.图像渲染问题 [https://leetcode.cn/problems/flood-fill/description/](https://leetcode.cn/problems/flood-fill/description/) class Solution {public int[][] floodFill(int[][] image, int sr, int sc, int color) {//可以借助另一…...
AI 时代 UI 设计的未来范式
在人工智能技术持续突破的浪潮下,UI 设计领域正经历着前所未有的变革。AI 的深度介入不仅重塑了设计流程,更催生了全新的设计范式,为用户带来颠覆式的交互体验。探索 AI 时代 UI 设计的未来范式,是把握行业发展趋势的关键所在。…...
键盘输出希腊字符方法
在不同操作系统中,输出希腊字母的方法有所不同。以下是针对 Windows 和 macOS 系统的详细方法,以及一些通用技巧: 1.Windows 系统 1.1 使用字符映射表 字符映射表是一个内置工具,可以方便地找到并插入希腊字母。 • 步骤…...
[数据结构高阶]并查集初识、手撕、可以解决哪类问题?
标题:[数据结构高阶]并查集初识、手撕、可以解决哪类问题? 水墨不写bug 文章目录 一、认识并查集二、模拟实现并查集三、用并查集解决问题1、[省份的数量](https://leetcode.cn/problems/number-of-provinces/)2、[等式方程的可满足性](https://leetcode…...
BUUCTF——PYWebsite
BUUCTF——PYWebsite 进入靶场 看看基本信息 没有什么信息 扫个目录看看 http://node5.buuoj.cn:28115/.DS_Store http://node5.buuoj.cn:28115/flag.php http://node5.buuoj.cn:28115/index.html访问flag.php 提示保存购买者的IP 抓包看看 直接XFF伪造一下 X-Forwarded-F…...
【学习笔记】机器学习(Machine Learning) | 第六章(2)| 过拟合问题
机器学习(Machine Learning) 简要声明 基于吴恩达教授(Andrew Ng)课程视频 BiliBili课程资源 文章目录 机器学习(Machine Learning)简要声明 解决过拟合问题一、收集更多训练数据二、选择特征三、正则化四、过拟合解决方法总结 过…...
单片机-STM32部分:13-1、编码器
飞书文档https://x509p6c8to.feishu.cn/wiki/BpEywhaX9iqbiLkdqdAcmDnwnab EC旋转编码器 在产品开发过程中,需要位置闭环的的产品,类似电机类产品来说,编码器至关重要,它不仅可以使我们对带年纪进行精确的速度闭环,位…...
浅谈大语言模型原理
1.反向传播算法 背景 反向传播算法是当前深度学习的核心技术。 神经网络 x是输入,o是输出,w是需要训练的参数(w有初始值)三层全连接的神经网络:输入层、隐藏层、输出层 激活函数 f ( x ) 1 1 x − 1 f(x)\frac…...
设计模式之中介者模式
在我们实际开发中,我们经常会遇到多个对象之间互相依赖、互相调用的场景。如果这些对象之间的耦合度太高,不仅会让系统变得难以维护,还会让扩展变得异常困难。此时,中介者模式(Mediatro)就是一种非常实用的设计方案,它…...
Matlab 空调温度时延模型的模糊pid控制
1、内容简介 Matlab 231-空调温度时延模型的模糊pid控制 可以交流、咨询、答疑 2、内容说明 略 3、仿真分析 略 4、参考论文 略中央空调温湿度采用PID控制系统的探讨.pdf 中央空调房间温度智能 PID控制的仿真研究.pdf...
RabbitMQ ③-Spring使用RabbitMQ
Spring使用RabbitMQ 创建 Spring 项目后,引入依赖: <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-amqp --> <dependency><groupId>org.springframework.boot</groupId><artifac…...
C++修炼:模板进阶
Hello大家好!很高兴我们又见面啦!给生活添点passion,开始今天的编程之路! 我的博客:<但凡. 我的专栏:《编程之路》、《数据结构与算法之美》、《题海拾贝》、《C修炼之路》 欢迎点赞,关注&am…...
Spring Boot集成RabbitMQ高级篇:可靠性与性能提升
一、环境准备 安装 RabbitMQ 在官网上下载对应操作系统的安装包(如 Windows、Linux 等),按照安装向导完成安装。 安装完成后,启动 RabbitMQ 服务。在 Windows 系统下,可以在服务列表中找到 RabbitMQ Server 并启动&am…...
Shell脚本编程3(函数+正则表达式)
1.函数 1.1 定义 简单来讲,所谓函数就是把完成特定功能,并且多次使用的一组命令或者语句封装在一个固定的结构中,这个结构我们就叫做函数。从本质上讲,函数是将一个函数名与某个代码块进行映射。也就是说,用户在定义了…...
【C++】语言深处的“精灵”:探索内存的奥妙
这里我们要知道,我们编写一个程序,这个程序中的变量是存储在哪个区域的 栈一般是用于静态的分配内存的,但也可以动态的分配内存, 堆是用于动态的分配内存的,不能静态的分配内存 栈: 通常是向低地址方向…...
c语言第一个小游戏:贪吃蛇小游戏03
我们为贪吃蛇的节点设置为一个结构体,构成贪吃蛇的身子的话我们使用链表,链表的每一个节点是一个结构体 显示贪吃蛇身子的一个节点 我们这边node就表示一个蛇的身体 就是一小节 输出结果如下 显示贪吃蛇完整身子 效果如下 代码实现 这个hasSnakeNode(…...
51 单片机头文件 reg51.h 和 reg52.h 详解
51 单片机头文件详解 51 单片机的头文件reg51.h和reg52.h是开发中非常重要的文件,它们定义了单片机的特殊功能寄存器 (SFR) 和位地址。以下是对这两个头文件的详细解析: 1. 头文件概述 reg51.h:针对标准 8051 单片机(4KB ROM, 128B RAM) reg52.h:针对增强型 8052 单片…...
让 - 艾里克・德布尔与斯普林格出版公司:科技变革下的出版业探索
在数字化浪潮席卷全球的当下,传统出版行业面临着前所未有的挑战与机遇。《对话 CTO,驾驭高科技浪潮》的第 10 章聚焦于让 - 艾里克・德布尔(Jean - Eric Debeure)及其所在的斯普林格出版公司(Springer Publishing Comp…...
[python] 面向对象的三大特性-封装及新式类
一 继承 继承是指一个类(子类)可以继承另一个类(父类)的属性和方法,并可以在其基础上进行扩展或修改。 子类可以继承父类的属性和方法,包括私有属性和隐藏属性 💡 核心思想: 避免重复代码&…...
winreg查询Windows注册表的一些基本用法
注册表是Windows操作系统中用于存储配置信息的数据库。它包含了关于系统硬件、已安装的应用程序、用户账户设置以及系统设置的信息。 特别地,当我们需要某些软件的配置配息时,主要在HKEY_CURRENT_USER和HKEY_LOCAL_MACHINE下的SoftWare内进行查询操作。 …...
DHCP自动分配IP
DHCP自动分配IP 练习1 路由器 Router>en Router#conf t Router(config)#ip dhcp pool ip10 //创建DHCP地址池 Router(dhcp-config)#network 192.168.20.0 255.255.255.0 // 配置网络地址和子网掩码 Router(dhcp-config)#default-router 192.168.20.254 //配置默认网关 Rou…...
互联网大厂Java求职面试实战:Spring Boot与微服务场景深度解析
💪🏻 1. Python基础专栏,基础知识一网打尽,9.9元买不了吃亏,买不了上当。 Python从入门到精通 😁 2. 毕业设计专栏,毕业季咱们不慌忙,几百款毕业设计等你选。 ❤️ 3. Python爬虫专栏…...
TDengine 在金融领域的应用
简介 金融行业正处于数据处理能力革新的关键时期。随着市场数据量的爆炸式增长和复杂性的日益加深,金融机构面临着寻找能够高效处理大规模、高频次以及多样化时序数据的大数据处理系统的迫切需求。这一选择将成为金融机构提高数据处理效率、优化交易响应时间、提高…...
十三、动态对象创建(Dynamic Object Creation)
十三、动态对象创建(Dynamic Object Creation) 目录 13.1 对象创建(Object creation)13.2 new / delete 操作符13.3 数组的 new 与 delete13.4 总结 背景说明 有时候我们需要知道程序中对象的数量、类型和声明周期,…...
cursor Too many报错 显示锁机器码怎么办?也就是Cursor的
22. Too many报错 显示锁机器码怎么办?也就是Cursor的 文档出自:https://www.kdocs.cn/l/cp5GpLHAWc0p...
window 显示驱动开发-将虚拟地址映射到内存段(二)
在将虚拟地址映射到段的一部分之前,视频内存管理器调用显示微型端口驱动程序的 DxgkDdiAcquireSwizzlingRange 函数,以便驱动程序可以设置用于访问可能重排的分配位的光圈。 驱动程序既不能将偏移量更改为访问分配的 PCI 光圈,也不能更改分配…...
Linux 软硬连接详解
目录 一、软链接(Symbolic Link) 定义与特性 实现方法使用 ln -s 命令: 二、硬链接(Hard Link) 1、是什么 2、工作机制 3、实现方式 一、软链接(Symbolic Link) 定义与特性 定义…...
虚拟主机与独立服务器:哪个更好?
在选择网站主机提供商时,你可以选择独立服务器或者与其他用户共同使用的虚拟主机。这个决定不仅仅是基于价格,还有很多其他因素需要考虑。接下来,我们就来详细了解一下虚拟主机和独立服务器的区别。 虚拟主机和独立服务器的区别 独立服务器是…...
MiMo-7B-RL调研
结论 MiMo 在数学推理和代码竞赛的评测中表现出色,但是相较于 OpenAI 的 o1-mini 和阿里的 QwQ-32B-Preview 等更大规模的模型,推理耗时更长(4 到 10 倍),花费 Token 更多。 链接 开源地址: https://huggingface.co/…...
vue-i18n 优化
语言包管理优化: 当前语言包文件(en.json 和 zh.json)过大,建议按模块拆分建议的目录结构: src/assets/i18n/ ├── modules/ │ ├── common/ │ ├── dashboard/ │ ├── report/ │ └── system/ …...