assert,util: improve deep object comparison performance

This improves the performance for almost all objects when comparing
them deeply.

PR-URL: https://github.com/nodejs/node/pull/57648
Reviewed-By: Jordan Harband <ljharb@gmail.com>
Reviewed-By: Bryan English <bryan@bryanenglish.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
This commit is contained in:
Ruben Bridgewater 2025-04-05 10:53:31 +02:00 committed by GitHub
parent 7e43337fdd
commit e739559e46
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 346 additions and 177 deletions

View File

@ -31,7 +31,7 @@ function benchmark(method, n, values, values2) {
}
function main({ n, len, method, strict }) {
const array = Array(len).fill(1);
const array = Array.from({ length: len }, () => '');
switch (method) {
case 'deepEqual_primitiveOnly': {

View File

@ -14,7 +14,6 @@ const primValues = {
'number': 1_000,
'boolean': true,
'object': { property: 'abcdef' },
'object_other_property': { property: 'abcdef' },
'array': [1, 2, 3],
'set_object': new Set([[1]]),
'set_simple': new Set([1, 2, 3]),

View File

@ -6,8 +6,9 @@ const { deepEqual, deepStrictEqual, notDeepEqual, notDeepStrictEqual } =
const bench = common.createBenchmark(main, {
n: [1e3],
len: [5e2],
len: [2, 1e2],
strict: [0, 1],
order: ['insert', 'random', 'reversed'],
method: [
'deepEqual_primitiveOnly',
'deepEqual_objectOnly',
@ -16,12 +17,30 @@ const bench = common.createBenchmark(main, {
'notDeepEqual_objectOnly',
'notDeepEqual_mixed',
],
}, {
combinationFilter(p) {
return p.order !== 'random' || p.strict === 1 && p.method !== 'notDeepEqual_objectOnly';
},
});
function benchmark(method, n, values, values2) {
function shuffleArray(array) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
const temp = array[i];
array[i] = array[j];
array[j] = temp;
}
}
function benchmark(method, n, values, values2, order) {
const actual = new Set(values);
// Prevent reference equal elements
const deepCopy = JSON.parse(JSON.stringify(values2 ? values2 : values));
let deepCopy = JSON.parse(JSON.stringify(values2));
if (order === 'reversed') {
deepCopy = deepCopy.reverse();
} else if (order === 'random') {
shuffleArray(deepCopy);
}
const expected = new Set(deepCopy);
bench.start();
for (let i = 0; i < n; ++i) {
@ -30,39 +49,39 @@ function benchmark(method, n, values, values2) {
bench.end(n);
}
function main({ n, len, method, strict }) {
const array = Array(len).fill(1);
function main({ n, len, method, strict, order }) {
const array = Array.from({ length: len }, () => '');
switch (method) {
case 'deepEqual_primitiveOnly': {
const values = array.map((_, i) => `str_${i}`);
benchmark(strict ? deepStrictEqual : deepEqual, n, values);
benchmark(strict ? deepStrictEqual : deepEqual, n, values, values, order);
break;
}
case 'deepEqual_objectOnly': {
const values = array.map((_, i) => [`str_${i}`, null]);
benchmark(strict ? deepStrictEqual : deepEqual, n, values);
benchmark(strict ? deepStrictEqual : deepEqual, n, values, values, order);
break;
}
case 'deepEqual_mixed': {
const values = array.map((_, i) => {
return i % 2 ? [`str_${i}`, null] : `str_${i}`;
});
benchmark(strict ? deepStrictEqual : deepEqual, n, values);
benchmark(strict ? deepStrictEqual : deepEqual, n, values, values, order);
break;
}
case 'notDeepEqual_primitiveOnly': {
const values = array.map((_, i) => `str_${i}`);
const values2 = values.slice(0);
values2[Math.floor(len / 2)] = 'w00t';
benchmark(strict ? notDeepStrictEqual : notDeepEqual, n, values, values2);
benchmark(strict ? notDeepStrictEqual : notDeepEqual, n, values, values2, order);
break;
}
case 'notDeepEqual_objectOnly': {
const values = array.map((_, i) => [`str_${i}`, null]);
const values2 = values.slice(0);
values2[Math.floor(len / 2)] = ['w00t'];
benchmark(strict ? notDeepStrictEqual : notDeepEqual, n, values, values2);
benchmark(strict ? notDeepStrictEqual : notDeepEqual, n, values, values2, order);
break;
}
case 'notDeepEqual_mixed': {
@ -71,7 +90,7 @@ function main({ n, len, method, strict }) {
});
const values2 = values.slice();
values2[0] = 'w00t';
benchmark(strict ? notDeepStrictEqual : notDeepEqual, n, values, values2);
benchmark(strict ? notDeepStrictEqual : notDeepEqual, n, values, values2, order);
break;
}
default:

View File

@ -62,7 +62,7 @@ function createSets(length, extraProps, depth = 0) {
number: i,
},
['array', 'with', 'values'],
!depth ? new Set([1, 2, { nested: i }]) : new Set(),
!depth ? new Set([1, { nested: i }]) : new Set(),
!depth ? createSets(2, extraProps, depth + 1) : null,
]));
}

View File

@ -9,6 +9,7 @@ const {
DatePrototypeGetTime,
Error,
NumberPrototypeValueOf,
ObjectGetOwnPropertyDescriptor,
ObjectGetOwnPropertySymbols: getOwnSymbols,
ObjectGetPrototypeOf,
ObjectIs,
@ -163,18 +164,6 @@ function isEnumerableOrIdentical(val1, val2, prop, mode, memos, method) {
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) {
@ -191,8 +180,7 @@ function innerDeepEqual(val1, val2, mode, memos) {
if (typeof val2 !== 'object' ||
typeof val1 !== 'object' ||
val1 === null ||
val2 === null ||
(mode === kStrict && ObjectGetPrototypeOf(val1) !== ObjectGetPrototypeOf(val2))) {
val2 === null) {
return false;
}
} else {
@ -206,6 +194,17 @@ function innerDeepEqual(val1, val2, mode, memos) {
return false;
}
}
return objectComparisonStart(val1, val2, mode, memos);
}
function objectComparisonStart(val1, val2, mode, memos) {
if (mode === kStrict &&
(val1.constructor !== val2.constructor ||
(val1.constructor === undefined &&
ObjectGetPrototypeOf(val1) !== ObjectGetPrototypeOf(val2)))) {
return false;
}
const val1Tag = ObjectPrototypeToString(val1);
const val2Tag = ObjectPrototypeToString(val2);
@ -214,7 +213,6 @@ function innerDeepEqual(val1, val2, mode, memos) {
}
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;
@ -316,6 +314,10 @@ function innerDeepEqual(val1, val2, mode, memos) {
isNativeError(val2) ||
val2 instanceof Error) {
return false;
} else if (isURL(val1)) {
if (!isURL(val2) || val1.href !== val2.href) {
return false;
}
} else if (isKeyObject(val1)) {
if (!isKeyObject(val2) || !val1.equals(val2)) {
return false;
@ -332,10 +334,6 @@ function innerDeepEqual(val1, val2, mode, memos) {
}
} 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);
@ -345,6 +343,21 @@ function getEnumerables(val, keys) {
return ArrayPrototypeFilter(keys, (key) => hasEnumerable(val, key));
}
function partialSymbolEquiv(val1, val2, keys2) {
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);
}
}
}
return true;
}
function keyCheck(val1, val2, mode, memos, iterationType, keys2) {
// For all remaining Object pairs, including Array, objects and Maps,
// equivalence is determined by having:
@ -358,31 +371,15 @@ function keyCheck(val1, val2, mode, memos, iterationType, keys2) {
if (keys2 === undefined) {
keys2 = ObjectKeys(val2);
}
// Cheap key test
if (keys2.length > 0) {
for (const key of keys2) {
if (!hasEnumerable(val1, key)) {
return false;
}
}
}
let keys1;
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);
}
}
if (!partialSymbolEquiv(val1, val2, keys2)) {
return false;
}
} else if (keys2.length !== ObjectKeys(val1).length) {
} else if (keys2.length !== (keys1 = ObjectKeys(val1)).length) {
return false;
} else if (mode === kStrict) {
const symbolKeysA = getOwnSymbols(val1);
@ -421,6 +418,13 @@ function keyCheck(val1, val2, mode, memos, iterationType, keys2) {
return true;
}
if (memos === null) {
return objEquiv(val1, val2, mode, keys1, keys2, memos, iterationType);
}
return handleCycles(val1, val2, mode, keys1, keys2, memos, iterationType);
}
function handleCycles(val1, val2, mode, keys1, keys2, memos, iterationType) {
// Use memos to handle cycles.
if (memos === undefined) {
memos = {
@ -431,7 +435,7 @@ function keyCheck(val1, val2, mode, memos, iterationType, keys2) {
d: undefined,
deep: false,
};
return objEquiv(val1, val2, mode, keys2, memos, iterationType);
return objEquiv(val1, val2, mode, keys1, keys2, memos, iterationType);
}
if (memos.set === undefined) {
@ -445,7 +449,7 @@ function keyCheck(val1, val2, mode, memos, iterationType, keys2) {
memos.c = val1;
memos.d = val2;
memos.deep = true;
const result = objEquiv(val1, val2, mode, keys2, memos, iterationType);
const result = objEquiv(val1, val2, mode, keys1, keys2, memos, iterationType);
memos.deep = false;
return result;
}
@ -465,7 +469,7 @@ function keyCheck(val1, val2, mode, memos, iterationType, keys2) {
return originalSize === set.size;
}
const areEq = objEquiv(val1, val2, mode, keys2, memos, iterationType);
const areEq = objEquiv(val1, val2, mode, keys1, keys2, memos, iterationType);
set.delete(val1);
set.delete(val2);
@ -473,18 +477,6 @@ function keyCheck(val1, val2, mode, memos, iterationType, keys2) {
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
@ -533,157 +525,269 @@ function mapMightHaveLoosePrim(a, b, prim, item2, memo) {
return !b.has(altValue) && innerDeepEqual(item1, item2, kLoose, memo);
}
function partialObjectSetEquiv(a, b, mode, set, memo) {
function partialObjectSetEquiv(array, a, b, mode, memo) {
let aPos = 0;
for (const val of a) {
let direction = 1;
let start = 0;
let end = array.length - 1;
for (const val1 of a) {
aPos++;
if (!b.has(val) && setHasEqualElement(set, val, mode, memo) && set.size === 0) {
return true;
if (!b.has(val1)) {
let innerStart = start;
if (direction === 1) {
if (innerDeepEqual(val1, array[start], mode, memo)) {
if (start === end) {
return true;
}
start += 1;
continue;
}
if (start === end) {
// The last element of set b might match a later element in set a.
continue;
}
direction = -1;
innerStart += 1;
}
let matched = true;
if (!innerDeepEqual(val1, array[end], mode, memo)) {
direction = 1;
matched = arrayHasEqualElement(array, val1, mode, memo, innerDeepEqual, innerStart, end);
}
if (matched) {
if (start === end) {
return true;
}
end -= 1;
}
}
if (a.size - aPos < set.size) {
if (a.size - aPos <= end - start) {
return false;
}
}
/* c8 ignore next */
assert.fail('Unreachable code');
return false;
}
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;
}
function arrayHasEqualElement(array, val1, mode, memo, comparator, start, end) {
let matched = false;
for (let i = end - 1; i >= start; i--) {
if (comparator(val1, array[i], mode, memo)) {
// Remove the matching element to make sure we do not check that again.
array.splice(i, 1);
matched = true;
break;
}
return true;
}
if (mode === kPartial) {
return partialObjectSetEquiv(a, b, mode, set, memo);
}
return matched;
}
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)) {
function setObjectEquiv(array, a, b, mode, memo) {
let direction = 1;
let start = 0;
let end = array.length - 1;
const comparator = mode !== kLoose ? objectComparisonStart : innerDeepEqual;
const extraChecks = mode === kLoose || array.length !== a.size;
for (const val1 of a) {
if (extraChecks) {
if (typeof val1 === 'object') {
if (b.has(val1)) {
continue;
}
} else if (mode !== kLoose || b.has(val1)) {
continue;
}
}
let innerStart = start;
if (direction === 1) {
if (comparator(val1, array[start], mode, memo)) {
start += 1;
continue;
}
if (start === end) {
return false;
}
} else if (mode === kLoose &&
!b.has(val) &&
!setHasEqualElement(set, val, mode, memo)) {
return false;
direction = -1;
innerStart += 1;
}
if (!comparator(val1, array[end], mode, memo)) {
direction = 1;
if (!arrayHasEqualElement(array, val1, mode, memo, comparator, innerStart, end)) {
return false;
}
}
end -= 1;
}
return set.size === 0;
return true;
}
function compareSmallSets(a, b, val, iteratorB, mode, memo) {
const iteratorA = a.values();
const firstA = iteratorA.next().value;
const first = innerDeepEqual(firstA, val, mode, memo);
if (first) {
if (b.size === 1) { // Partial mode && a.size === 1 || b.size === 1
return true;
}
const secondA = iteratorA.next().value;
return b.has(secondA) || innerDeepEqual(secondA, iteratorB.next().value, mode, memo);
}
return a.size !== 1 && innerDeepEqual(iteratorA.next().value, val, mode, memo) && (
b.size === 1 || // Partial mode
b.has(firstA) || // Primitive or reference equal
innerDeepEqual(firstA, iteratorB.next().value, mode, memo)
);
}
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) {
let array;
const iteratorB = b.values();
for (const val of iteratorB) {
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);
if (array === undefined) {
if (a.size < 3) {
return compareSmallSets(a, b, val, iteratorB, mode, memo);
}
set = new SafeSet();
array = [];
}
// 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);
array.push(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;
}
}
if (array === undefined) {
return true;
}
if (mode === kPartial) {
return partialObjectMapEquiv(a, b, mode, set, memo);
return partialObjectSetEquiv(array, a, b, mode, memo);
}
return setObjectEquiv(array, a, b, mode, memo);
}
function partialObjectMapEquiv(array, a, b, mode, memo) {
let aPos = 0;
let direction = 1;
let start = 0;
let end = array.length - 1;
for (const { 0: key1, 1: item1 } of a) {
aPos++;
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)) {
let innerStart = start;
if (direction === 1) {
const key2 = array[start];
if (objectComparisonStart(key1, key2, mode, memo) && innerDeepEqual(item1, b.get(key2), mode, memo)) {
if (start === end) {
return true;
}
start += 1;
continue;
}
if (start === end) {
// The last element of map b might match a later element in map a.
continue;
}
direction = -1;
innerStart += 1;
}
let matched = true;
const key2 = array[end];
if (!objectComparisonStart(key1, key2, mode, memo) || !innerDeepEqual(item1, b.get(key2), mode, memo)) {
direction = 1;
matched = arrayHasEqualMapElement(array, key1, item1, b, mode, memo, objectComparisonStart, innerStart, end);
}
if (matched) {
if (start === end) {
return true;
}
end -= 1;
}
}
if (a.size - aPos <= end - start) {
return false;
}
}
return set.size === 0;
return false;
}
function arrayHasEqualMapElement(array, key1, item1, b, mode, memo, comparator, start, end) {
let matched = false;
for (let i = end - 1; i >= start; i--) {
const key2 = array[i];
if (comparator(key1, key2, mode, memo) &&
innerDeepEqual(item1, b.get(key2), mode, memo)) {
// Remove the matching element to make sure we do not check that again.
array.splice(i, 1);
matched = true;
break;
}
}
return matched;
}
function mapObjectEquiv(array, a, b, mode, memo) {
let direction = 1;
let start = 0;
let end = array.length - 1;
const comparator = mode !== kLoose ? objectComparisonStart : innerDeepEqual;
const extraChecks = mode === kLoose || array.length !== a.size;
for (const { 0: key1, 1: item1 } of a) {
if (extraChecks &&
(typeof key1 !== 'object' || key1 === null) &&
(mode !== kLoose ||
(b.has(key1) && innerDeepEqual(item1, b.get(key1), mode, memo)))) { // Mixed mode
continue;
}
let innerStart = start;
if (direction === 1) {
const key2 = array[start];
if (comparator(key1, key2, mode, memo) && innerDeepEqual(item1, b.get(key2), mode, memo)) {
start += 1;
continue;
}
if (start === end) {
return false;
}
direction = -1;
innerStart += 1;
}
const key2 = array[end];
if ((!comparator(key1, key2, mode, memo) || !innerDeepEqual(item1, b.get(key2), mode, memo))) {
direction = 1;
if (!arrayHasEqualMapElement(array, key1, item1, b, mode, memo, comparator, innerStart, end)) {
return false;
}
}
end -= 1;
}
return true;
}
function mapEquiv(a, b, mode, memo) {
let set = null;
let array;
for (const { 0: key2, 1: item2 } of b) {
if (typeof key2 === 'object' && key2 !== null) {
if (set === null) {
if (array === undefined) {
if (a.size === 1) {
const { 0: key1, 1: item1 } = a.entries().next().value;
return innerDeepEqual(key1, key2, mode, memo) &&
innerDeepEqual(item1, item2, mode, memo);
innerDeepEqual(item1, item2, mode, memo);
}
set = new SafeSet();
array = [];
}
set.add(key2);
array.push(key2);
} else {
// By directly retrieving the value we prevent another b.has(key2) check in
// almost all possible cases.
@ -696,19 +800,23 @@ function mapEquiv(a, b, mode, memo) {
// keys.
if (!mapMightHaveLoosePrim(a, b, key2, item2, memo))
return false;
if (set === null) {
set = new SafeSet();
if (array === undefined) {
array = [];
}
set.add(key2);
array.push(key2);
}
}
}
if (set !== null) {
return mapObjectEquivalence(a, b, mode, set, memo);
if (array === undefined) {
return true;
}
return true;
if (mode === kPartial) {
return partialObjectMapEquiv(array, a, b, mode, memo);
}
return mapObjectEquiv(array, a, b, mode, memo);
}
function partialSparseArrayEquiv(a, b, mode, memos, startA, startB) {
@ -770,11 +878,31 @@ function sparseArrayEquiv(a, b, mode, memos, i) {
return true;
}
function objEquiv(a, b, mode, keys2, memos, iterationType) {
function objEquiv(a, b, mode, keys1, 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)) {
let i = 0;
// Ordered keys
if (keys1 !== undefined) {
for (; i < keys2.length; i++) {
const key = keys2[i];
if (keys1[i] !== key) {
break;
}
if (!innerDeepEqual(a[key], b[key], mode, memos)) {
return false;
}
}
}
// Unordered keys
for (; i < keys2.length; i++) {
const key = keys2[i];
// It is faster to get the whole descriptor and to check it's enumerable
// property in V8 13.0 compared to calling Object.propertyIsEnumerable()
// and accessing the property regularly.
const descriptor = ObjectGetOwnPropertyDescriptor(a, key);
if (!descriptor?.enumerable ||
!innerDeepEqual(descriptor.value !== undefined ? descriptor.value : a[key], b[key], mode, memos)) {
return false;
}
}
@ -807,14 +935,25 @@ function objEquiv(a, b, mode, keys2, memos, iterationType) {
return true;
}
// Only handle cycles when they are detected.
// eslint-disable-next-line func-style
let detectCycles = function(val1, val2, mode) {
try {
return innerDeepEqual(val1, val2, mode, null);
} catch {
detectCycles = innerDeepEqual;
return innerDeepEqual(val1, val2, mode, undefined);
}
};
module.exports = {
isDeepEqual(val1, val2) {
return innerDeepEqual(val1, val2, kLoose);
return detectCycles(val1, val2, kLoose);
},
isDeepStrictEqual(val1, val2) {
return innerDeepEqual(val1, val2, kStrict);
return detectCycles(val1, val2, kStrict);
},
isPartialStrictEqual(val1, val2) {
return innerDeepEqual(val1, val2, kPartial);
return detectCycles(val1, val2, kPartial);
},
};

View File

@ -406,6 +406,18 @@ test('es6 Maps and Sets', () => {
new Set([xarray, ['y']]),
new Set([xarray, ['y']])
);
assertDeepAndStrictEqual(
new Set([2, xarray, ['y'], 1]),
new Set([xarray, ['y'], 1, 2])
);
assertDeepAndStrictEqual(
new Set([{ a: 1 }, { a: 3 }, { a: 2 }, { a: 4 }]),
new Set([{ a: 2 }, { a: 1 }, { a: 4 }, { a: 3 }])
);
assertNotDeepOrStrict(
new Set([{ a: 1 }, { a: 2 }, { a: 3 }, { a: 4 }]),
new Set([{ a: 1 }, { a: 2 }, { a: 3 }, { a: 5 }])
);
assertOnlyDeepEqual(
new Set([null, '', 1n, 5, 2n, false]),
new Set([undefined, 0, 5n, true, '2', '-000'])