node/lib/internal/util/comparisons.js
Ruben Bridgewater b9c9bf4945 assert,util: improve array comparison
Sparse arrays and arrays containing undefined are now compared faster
using assert.deepStrictEqual() or util.isDeepStrictEqual().

PR-URL: https://github.com/nodejs/node/pull/57619
Reviewed-By: Jordan Harband <ljharb@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Chemi Atlow <chemi@atlow.co.il>
Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
Reviewed-By: Edy Silva <edigleyssonsilva@gmail.com>
Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com>
Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com>
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
2025-04-02 23:45:36 +00:00

816 lines
23 KiB
JavaScript

'use strict';
const {
ArrayIsArray,
ArrayPrototypeFilter,
ArrayPrototypePush,
BigIntPrototypeValueOf,
BooleanPrototypeValueOf,
DatePrototypeGetTime,
Error,
NumberIsNaN,
NumberPrototypeValueOf,
ObjectGetOwnPropertySymbols: getOwnSymbols,
ObjectGetPrototypeOf,
ObjectIs,
ObjectKeys,
ObjectPrototypeHasOwnProperty: hasOwn,
ObjectPrototypePropertyIsEnumerable: hasEnumerable,
ObjectPrototypeToString,
SafeSet,
StringPrototypeValueOf,
SymbolPrototypeValueOf,
TypedArrayPrototypeGetByteLength: getByteLength,
TypedArrayPrototypeGetSymbolToStringTag,
Uint8Array,
} = primordials;
const { compare } = internalBinding('buffer');
const assert = require('internal/assert');
const { isURL } = require('internal/url');
const { isError } = require('internal/util');
const types = require('internal/util/types');
const {
isAnyArrayBuffer,
isArrayBufferView,
isDate,
isMap,
isRegExp,
isSet,
isNativeError,
isBoxedPrimitive,
isNumberObject,
isStringObject,
isBooleanObject,
isBigIntObject,
isSymbolObject,
isFloat32Array,
isFloat64Array,
isKeyObject,
isCryptoKey,
isWeakMap,
isWeakSet,
} = types;
const {
constants: {
ONLY_ENUMERABLE,
SKIP_SYMBOLS,
},
getOwnNonIndexProperties,
} = internalBinding('util');
const kStrict = 1;
const kLoose = 0;
const kPartial = 2;
const kNoIterator = 0;
const kIsArray = 1;
const kIsSet = 2;
const kIsMap = 3;
let kKeyObject;
// Check if they have the same source and flags
function areSimilarRegExps(a, b) {
return a.source === b.source &&
a.flags === b.flags &&
a.lastIndex === b.lastIndex;
}
function isPartialUint8Array(a, b) {
const lenA = getByteLength(a);
const lenB = getByteLength(b);
if (lenA < lenB) {
return false;
}
let offsetA = 0;
for (let offsetB = 0; offsetB < lenB; offsetB++) {
while (!ObjectIs(a[offsetA], b[offsetB])) {
offsetA++;
if (offsetA > lenA - lenB + offsetB) {
return false;
}
}
offsetA++;
}
return true;
}
function isPartialArrayBufferView(a, b) {
if (a.byteLength < b.byteLength) {
return false;
}
return isPartialUint8Array(
new Uint8Array(a.buffer, a.byteOffset, a.byteLength),
new Uint8Array(b.buffer, b.byteOffset, b.byteLength),
);
}
function areSimilarFloatArrays(a, b) {
const len = getByteLength(a);
if (len !== getByteLength(b)) {
return false;
}
for (let offset = 0; offset < len; offset++) {
if (a[offset] !== b[offset]) {
return false;
}
}
return true;
}
function areSimilarTypedArrays(a, b) {
if (a.byteLength !== b.byteLength) {
return false;
}
return compare(new Uint8Array(a.buffer, a.byteOffset, a.byteLength),
new Uint8Array(b.buffer, b.byteOffset, b.byteLength)) === 0;
}
function areEqualArrayBuffers(buf1, buf2) {
return buf1.byteLength === buf2.byteLength &&
compare(new Uint8Array(buf1), new Uint8Array(buf2)) === 0;
}
function isEqualBoxedPrimitive(val1, val2) {
if (isNumberObject(val1)) {
return isNumberObject(val2) &&
ObjectIs(NumberPrototypeValueOf(val1),
NumberPrototypeValueOf(val2));
}
if (isStringObject(val1)) {
return isStringObject(val2) &&
StringPrototypeValueOf(val1) === StringPrototypeValueOf(val2);
}
if (isBooleanObject(val1)) {
return isBooleanObject(val2) &&
BooleanPrototypeValueOf(val1) === BooleanPrototypeValueOf(val2);
}
if (isBigIntObject(val1)) {
return isBigIntObject(val2) &&
BigIntPrototypeValueOf(val1) === BigIntPrototypeValueOf(val2);
}
if (isSymbolObject(val1)) {
return isSymbolObject(val2) &&
SymbolPrototypeValueOf(val1) === SymbolPrototypeValueOf(val2);
}
/* c8 ignore next */
assert.fail(`Unknown boxed type ${val1}`);
}
function isEnumerableOrIdentical(val1, val2, prop, mode, memos, method) {
return hasEnumerable(val2, prop) || // This is handled by Object.keys()
(mode === kPartial && (val2[prop] === undefined || (prop === 'message' && val2[prop] === ''))) ||
innerDeepEqual(val1[prop], val2[prop], mode, memos);
}
// Notes: Type tags are historical [[Class]] properties that can be set by
// FunctionTemplate::SetClassName() in C++ or Symbol.toStringTag in JS
// and retrieved using Object.prototype.toString.call(obj) in JS
// See https://tc39.github.io/ecma262/#sec-object.prototype.tostring
// for a list of tags pre-defined in the spec.
// There are some unspecified tags in the wild too (e.g. typed array tags).
// Since tags can be altered, they only serve fast failures
//
// For strict comparison, objects should have
// a) The same built-in type tag.
// b) The same prototypes.
function innerDeepEqual(val1, val2, mode, memos) {
// All identical values are equivalent, as determined by ===.
if (val1 === val2) {
return val1 !== 0 || ObjectIs(val1, val2) || mode === kLoose;
}
// Check more closely if val1 and val2 are equal.
if (mode !== kLoose) {
if (typeof val1 === 'number') {
return NumberIsNaN(val1) && NumberIsNaN(val2);
}
if (typeof val2 !== 'object' ||
typeof val1 !== 'object' ||
val1 === null ||
val2 === null ||
(mode === kStrict && ObjectGetPrototypeOf(val1) !== ObjectGetPrototypeOf(val2))) {
return false;
}
} else {
if (val1 === null || typeof val1 !== 'object') {
return (val2 === null || typeof val2 !== 'object') &&
// eslint-disable-next-line eqeqeq
(val1 == val2 || (NumberIsNaN(val1) && NumberIsNaN(val2)));
}
if (val2 === null || typeof val2 !== 'object') {
return false;
}
}
const val1Tag = ObjectPrototypeToString(val1);
const val2Tag = ObjectPrototypeToString(val2);
if (val1Tag !== val2Tag) {
return false;
}
if (ArrayIsArray(val1)) {
// Check for sparse arrays and general fast path
if (!ArrayIsArray(val2) ||
(val1.length !== val2.length && (mode !== kPartial || val1.length < val2.length))) {
return false;
}
const filter = mode !== kLoose ? ONLY_ENUMERABLE : ONLY_ENUMERABLE | SKIP_SYMBOLS;
const keys2 = getOwnNonIndexProperties(val2, filter);
if (mode !== kPartial &&
keys2.length !== getOwnNonIndexProperties(val1, filter).length) {
return false;
}
return keyCheck(val1, val2, mode, memos, kIsArray, keys2);
} else if (val1Tag === '[object Object]') {
return keyCheck(val1, val2, mode, memos, kNoIterator);
} else if (isDate(val1)) {
if (!isDate(val2) ||
DatePrototypeGetTime(val1) !== DatePrototypeGetTime(val2)) {
return false;
}
} else if (isRegExp(val1)) {
if (!isRegExp(val2) || !areSimilarRegExps(val1, val2)) {
return false;
}
} else if (isArrayBufferView(val1)) {
if (TypedArrayPrototypeGetSymbolToStringTag(val1) !==
TypedArrayPrototypeGetSymbolToStringTag(val2)) {
return false;
}
if (mode === kPartial && val1.byteLength !== val2.byteLength) {
if (!isPartialArrayBufferView(val1, val2)) {
return false;
}
} else if (mode === kLoose && (isFloat32Array(val1) || isFloat64Array(val1))) {
if (!areSimilarFloatArrays(val1, val2)) {
return false;
}
} else if (!areSimilarTypedArrays(val1, val2)) {
return false;
}
// Buffer.compare returns true, so val1.length === val2.length. If they both
// only contain numeric keys, we don't need to exam further than checking
// the symbols.
const filter = mode !== kLoose ? ONLY_ENUMERABLE : ONLY_ENUMERABLE | SKIP_SYMBOLS;
const keys2 = getOwnNonIndexProperties(val2, filter);
if (mode !== kPartial &&
keys2.length !== getOwnNonIndexProperties(val1, filter).length) {
return false;
}
return keyCheck(val1, val2, mode, memos, kNoIterator, keys2);
} else if (isSet(val1)) {
if (!isSet(val2) ||
(val1.size !== val2.size && (mode !== kPartial || val1.size < val2.size))) {
return false;
}
return keyCheck(val1, val2, mode, memos, kIsSet);
} else if (isMap(val1)) {
if (!isMap(val2) ||
(val1.size !== val2.size && (mode !== kPartial || val1.size < val2.size))) {
return false;
}
return keyCheck(val1, val2, mode, memos, kIsMap);
} else if (isAnyArrayBuffer(val1)) {
if (!isAnyArrayBuffer(val2)) {
return false;
}
if (mode !== kPartial || val1.byteLength === val2.byteLength) {
if (!areEqualArrayBuffers(val1, val2)) {
return false;
}
} else if (!isPartialUint8Array(new Uint8Array(val1), new Uint8Array(val2))) {
return false;
}
} else if (isError(val1)) {
// Do not compare the stack as it might differ even though the error itself
// is otherwise identical.
if (!isError(val2) ||
!isEnumerableOrIdentical(val1, val2, 'message', mode, memos) ||
!isEnumerableOrIdentical(val1, val2, 'name', mode, memos) ||
!isEnumerableOrIdentical(val1, val2, 'cause', mode, memos) ||
!isEnumerableOrIdentical(val1, val2, 'errors', mode, memos)) {
return false;
}
const hasOwnVal2Cause = hasOwn(val2, 'cause');
if ((hasOwnVal2Cause !== hasOwn(val1, 'cause') && (mode !== kPartial || hasOwnVal2Cause))) {
return false;
}
} else if (isBoxedPrimitive(val1)) {
if (!isEqualBoxedPrimitive(val1, val2)) {
return false;
}
} else if (ArrayIsArray(val2) ||
isArrayBufferView(val2) ||
isSet(val2) ||
isMap(val2) ||
isDate(val2) ||
isRegExp(val2) ||
isAnyArrayBuffer(val2) ||
isBoxedPrimitive(val2) ||
isNativeError(val2) ||
val2 instanceof Error) {
return false;
} else if (isKeyObject(val1)) {
if (!isKeyObject(val2) || !val1.equals(val2)) {
return false;
}
} else if (isCryptoKey(val1)) {
kKeyObject ??= require('internal/crypto/util').kKeyObject;
if (!isCryptoKey(val2) ||
val1.extractable !== val2.extractable ||
!innerDeepEqual(val1.algorithm, val2.algorithm, mode, memos) ||
!innerDeepEqual(val1.usages, val2.usages, mode, memos) ||
!innerDeepEqual(val1[kKeyObject], val2[kKeyObject], mode, memos)
) {
return false;
}
} else if (isWeakMap(val1) || isWeakSet(val1)) {
return false;
} else if (isURL(val1)) {
if (!isURL(val2) || val1.href !== val2.href) {
return false;
}
}
return keyCheck(val1, val2, mode, memos, kNoIterator);
}
function getEnumerables(val, keys) {
return ArrayPrototypeFilter(keys, (key) => hasEnumerable(val, key));
}
function keyCheck(val1, val2, mode, memos, iterationType, keys2) {
// For all remaining Object pairs, including Array, objects and Maps,
// equivalence is determined by having:
// a) The same number of owned enumerable properties
// b) The same set of keys/indexes (although not necessarily the same order)
// c) Equivalent values for every corresponding key/index
// d) For Sets and Maps, equal contents
// Note: this accounts for both named and indexed properties on Arrays.
const isArrayLikeObject = keys2 !== undefined;
if (keys2 === undefined) {
keys2 = ObjectKeys(val2);
}
// Cheap key test
if (keys2.length > 0) {
for (const key of keys2) {
if (!hasEnumerable(val1, key)) {
return false;
}
}
}
if (!isArrayLikeObject) {
// The pair must have the same number of owned properties.
if (mode === kPartial) {
const symbolKeys = getOwnSymbols(val2);
if (symbolKeys.length !== 0) {
for (const key of symbolKeys) {
if (hasEnumerable(val2, key)) {
if (!hasEnumerable(val1, key)) {
return false;
}
ArrayPrototypePush(keys2, key);
}
}
}
} else if (keys2.length !== ObjectKeys(val1).length) {
return false;
} else if (mode === kStrict) {
const symbolKeysA = getOwnSymbols(val1);
if (symbolKeysA.length !== 0) {
let count = 0;
for (const key of symbolKeysA) {
if (hasEnumerable(val1, key)) {
if (!hasEnumerable(val2, key)) {
return false;
}
ArrayPrototypePush(keys2, key);
count++;
} else if (hasEnumerable(val2, key)) {
return false;
}
}
const symbolKeysB = getOwnSymbols(val2);
if (symbolKeysA.length !== symbolKeysB.length &&
getEnumerables(val2, symbolKeysB).length !== count) {
return false;
}
} else {
const symbolKeysB = getOwnSymbols(val2);
if (symbolKeysB.length !== 0 &&
getEnumerables(val2, symbolKeysB).length !== 0) {
return false;
}
}
}
}
if (keys2.length === 0 &&
(iterationType === kNoIterator ||
(iterationType === kIsArray && val2.length === 0) ||
val2.size === 0)) {
return true;
}
// Use memos to handle cycles.
if (memos === undefined) {
memos = {
set: undefined,
a: val1,
b: val2,
c: undefined,
d: undefined,
deep: false,
};
return objEquiv(val1, val2, mode, keys2, memos, iterationType);
}
if (memos.set === undefined) {
if (memos.deep === false) {
if (memos.a === val1) {
return memos.b === val2;
}
if (memos.b === val2) {
return false;
}
memos.c = val1;
memos.d = val2;
memos.deep = true;
const result = objEquiv(val1, val2, mode, keys2, memos, iterationType);
memos.deep = false;
return result;
}
memos.set = new SafeSet();
memos.set.add(memos.a);
memos.set.add(memos.b);
memos.set.add(memos.c);
memos.set.add(memos.d);
}
const { set } = memos;
const originalSize = set.size;
set.add(val1);
set.add(val2);
if (originalSize !== set.size - 2) {
return originalSize === set.size;
}
const areEq = objEquiv(val1, val2, mode, keys2, memos, iterationType);
set.delete(val1);
set.delete(val2);
return areEq;
}
function setHasEqualElement(set, val1, mode, memo) {
for (const val2 of set) {
if (innerDeepEqual(val1, val2, mode, memo)) {
// Remove the matching element to make sure we do not check that again.
set.delete(val2);
return true;
}
}
return false;
}
// See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Equality_comparisons_and_sameness#Loose_equality_using
// Sadly it is not possible to detect corresponding values properly in case the
// type is a string, number, bigint or boolean. The reason is that those values
// can match lots of different string values (e.g., 1n == '+00001').
function findLooseMatchingPrimitives(prim) {
switch (typeof prim) {
case 'undefined':
return null;
case 'object': // Only pass in null as object!
return undefined;
case 'symbol':
return false;
case 'string':
prim = +prim;
// Loose equal entries exist only if the string is possible to convert to
// a regular number and not NaN.
// Fall through
case 'number':
if (NumberIsNaN(prim)) {
return false;
}
}
return true;
}
function setMightHaveLoosePrim(a, b, prim) {
const altValue = findLooseMatchingPrimitives(prim);
if (altValue != null)
return altValue;
return !b.has(altValue) && a.has(altValue);
}
function mapMightHaveLoosePrim(a, b, prim, item2, memo) {
const altValue = findLooseMatchingPrimitives(prim);
if (altValue != null) {
return altValue;
}
const item1 = a.get(altValue);
if ((item1 === undefined && !a.has(altValue)) ||
!innerDeepEqual(item1, item2, kLoose, memo)) {
return false;
}
return !b.has(altValue) && innerDeepEqual(item1, item2, kLoose, memo);
}
function partialObjectSetEquiv(a, b, mode, set, memo) {
let aPos = 0;
for (const val of a) {
aPos++;
if (!b.has(val) && setHasEqualElement(set, val, mode, memo) && set.size === 0) {
return true;
}
if (a.size - aPos < set.size) {
return false;
}
}
/* c8 ignore next */
assert.fail('Unreachable code');
}
function setObjectEquiv(a, b, mode, set, memo) {
// Fast path for objects only
if (mode !== kLoose && set.size === a.size) {
for (const val of a) {
if (!setHasEqualElement(set, val, mode, memo)) {
return false;
}
}
return true;
}
if (mode === kPartial) {
return partialObjectSetEquiv(a, b, mode, set, memo);
}
for (const val of a) {
// Primitive values have already been handled above.
if (typeof val === 'object') {
if (!b.has(val) && !setHasEqualElement(set, val, mode, memo)) {
return false;
}
} else if (mode === kLoose &&
!b.has(val) &&
!setHasEqualElement(set, val, mode, memo)) {
return false;
}
}
return set.size === 0;
}
function setEquiv(a, b, mode, memo) {
// This is a lazily initiated Set of entries which have to be compared
// pairwise.
let set = null;
for (const val of b) {
if (!a.has(val)) {
if ((typeof val !== 'object' || val === null) &&
(mode !== kLoose || !setMightHaveLoosePrim(a, b, val))) {
return false;
}
if (set === null) {
if (a.size === 1) {
return innerDeepEqual(a.values().next().value, val, mode, memo);
}
set = new SafeSet();
}
// If the specified value doesn't exist in the second set it's a object
// (or in loose mode: a non-matching primitive). Find the
// deep-(mode-)equal element in a set copy to reduce duplicate checks.
set.add(val);
}
}
if (set !== null) {
return setObjectEquiv(a, b, mode, set, memo);
}
return true;
}
function mapHasEqualEntry(set, map, key1, item1, mode, memo) {
// To be able to handle cases like:
// Map([[{}, 'a'], [{}, 'b']]) vs Map([[{}, 'b'], [{}, 'a']])
// ... we need to consider *all* matching keys, not just the first we find.
for (const key2 of set) {
if (innerDeepEqual(key1, key2, mode, memo) &&
innerDeepEqual(item1, map.get(key2), mode, memo)) {
set.delete(key2);
return true;
}
}
return false;
}
function partialObjectMapEquiv(a, b, mode, set, memo) {
let aPos = 0;
for (const { 0: key1, 1: item1 } of a) {
aPos++;
if (typeof key1 === 'object' &&
key1 !== null &&
mapHasEqualEntry(set, b, key1, item1, mode, memo) &&
set.size === 0) {
return true;
}
if (a.size - aPos < set.size) {
return false;
}
}
/* c8 ignore next */
assert.fail('Unreachable code');
}
function mapObjectEquivalence(a, b, mode, set, memo) {
// Fast path for objects only
if (mode !== kLoose && set.size === a.size) {
for (const { 0: key1, 1: item1 } of a) {
if (!mapHasEqualEntry(set, b, key1, item1, mode, memo)) {
return false;
}
}
return true;
}
if (mode === kPartial) {
return partialObjectMapEquiv(a, b, mode, set, memo);
}
for (const { 0: key1, 1: item1 } of a) {
if (typeof key1 === 'object' && key1 !== null) {
if (!mapHasEqualEntry(set, b, key1, item1, mode, memo))
return false;
} else if (set.size === 0) {
return true;
} else if (mode === kLoose &&
(!b.has(key1) ||
!innerDeepEqual(item1, b.get(key1), mode, memo)) &&
!mapHasEqualEntry(set, b, key1, item1, mode, memo)) {
return false;
}
}
return set.size === 0;
}
function mapEquiv(a, b, mode, memo) {
let set = null;
for (const { 0: key2, 1: item2 } of b) {
if (typeof key2 === 'object' && key2 !== null) {
if (set === null) {
if (a.size === 1) {
const { 0: key1, 1: item1 } = a.entries().next().value;
return innerDeepEqual(key1, key2, mode, memo) &&
innerDeepEqual(item1, item2, mode, memo);
}
set = new SafeSet();
}
set.add(key2);
} else {
// By directly retrieving the value we prevent another b.has(key2) check in
// almost all possible cases.
const item1 = a.get(key2);
if (((item1 === undefined && !a.has(key2)) ||
!innerDeepEqual(item1, item2, mode, memo))) {
if (mode !== kLoose)
return false;
// Fast path to detect missing string, symbol, undefined and null
// keys.
if (!mapMightHaveLoosePrim(a, b, key2, item2, memo))
return false;
if (set === null) {
set = new SafeSet();
}
set.add(key2);
}
}
}
if (set !== null) {
return mapObjectEquivalence(a, b, mode, set, memo);
}
return true;
}
function partialSparseArrayEquiv(a, b, mode, memos, startA, startB) {
let aPos = 0;
const keysA = ObjectKeys(a).slice(startA);
const keysB = ObjectKeys(b).slice(startB);
if (keysA.length < keysB.length) {
return false;
}
for (let i = 0; i < keysB.length; i++) {
const keyB = keysB[i];
while (!innerDeepEqual(a[keysA[aPos]], b[keyB], mode, memos)) {
aPos++;
if (aPos > keysA.length - keysB.length + i) {
return false;
}
}
aPos++;
}
return true;
}
function partialArrayEquiv(a, b, mode, memos) {
let aPos = 0;
for (let i = 0; i < b.length; i++) {
let isSparse = b[i] === undefined && !hasOwn(b, i);
if (isSparse) {
return partialSparseArrayEquiv(a, b, mode, memos, aPos, i);
}
while (!(isSparse = a[aPos] === undefined && !hasOwn(a, aPos)) &&
!innerDeepEqual(a[aPos], b[i], mode, memos)) {
aPos++;
if (aPos > a.length - b.length + i) {
return false;
}
}
if (isSparse) {
return partialSparseArrayEquiv(a, b, mode, memos, aPos, i);
}
aPos++;
}
return true;
}
function sparseArrayEquiv(a, b, mode, memos, i) {
// TODO(BridgeAR): Use internal method to only get index properties. The
// same applies to the partial implementation.
const keysA = ObjectKeys(a);
const keysB = ObjectKeys(b);
if (keysA.length !== keysB.length) {
return false;
}
for (; i < keysB.length; i++) {
const key = keysB[i];
if ((a[key] === undefined && !hasOwn(a, key)) || !innerDeepEqual(a[key], b[key], mode, memos)) {
return false;
}
}
return true;
}
function objEquiv(a, b, mode, keys2, memos, iterationType) {
// The pair must have equivalent values for every corresponding key.
if (keys2.length > 0) {
for (const key of keys2) {
if (!innerDeepEqual(a[key], b[key], mode, memos)) {
return false;
}
}
}
if (iterationType === kIsArray) {
if (mode === kPartial) {
return partialArrayEquiv(a, b, mode, memos);
}
for (let i = 0; i < a.length; i++) {
if (b[i] === undefined) {
if (!hasOwn(b, i))
return sparseArrayEquiv(a, b, mode, memos, i);
if (a[i] !== undefined || !hasOwn(a, i))
return false;
} else if (a[i] === undefined || !innerDeepEqual(a[i], b[i], mode, memos)) {
return false;
}
}
} else if (iterationType === kIsSet) {
if (!setEquiv(a, b, mode, memos)) {
return false;
}
} else if (iterationType === kIsMap) {
if (!mapEquiv(a, b, mode, memos)) {
return false;
}
}
return true;
}
module.exports = {
isDeepEqual(val1, val2) {
return innerDeepEqual(val1, val2, kLoose);
},
isDeepStrictEqual(val1, val2) {
return innerDeepEqual(val1, val2, kStrict);
},
isPartialStrictEqual(val1, val2) {
return innerDeepEqual(val1, val2, kPartial);
},
};