From fcf1af5c263d9e86359f63331553fa49bef7a6d9 Mon Sep 17 00:00:00 2001 From: Izel Nakri Date: Tue, 16 Aug 2022 03:45:06 +0200 Subject: [PATCH 01/14] Core: move equiv private functions to module cache --- src/equiv.js | 520 +++++++++++++++++++++++++-------------------------- 1 file changed, 258 insertions(+), 262 deletions(-) diff --git a/src/equiv.js b/src/equiv.js index 83f05f196..e69170cc9 100644 --- a/src/equiv.js +++ b/src/equiv.js @@ -1,320 +1,316 @@ import { objectType } from './core/utilities'; -// Test for equality any JavaScript type. -// Authors: Philippe Rathé , David Chan -export default (function () { - // Value pairs queued for comparison. Used for breadth-first processing order, recursion - // detection and avoiding repeated comparison (see below for details). - // Elements are { a: val, b: val }. - let pairs = []; - - function useStrictEquality (a, b) { - // This only gets called if a and b are not strict equal, and is used to compare on - // the primitive values inside object wrappers. For example: - // `var i = 1;` - // `var j = new Number(1);` - // Neither a nor b can be null, as a !== b and they have the same type. - if (typeof a === 'object') { - a = a.valueOf(); - } - if (typeof b === 'object') { - b = b.valueOf(); - } - - return a === b; +// Value pairs queued for comparison. Used for breadth-first processing order, recursion +// detection and avoiding repeated comparison (see below for details). +// Elements are { a: val, b: val }. +let pairs = []; + +function useStrictEquality (a, b) { + // This only gets called if a and b are not strict equal, and is used to compare on + // the primitive values inside object wrappers. For example: + // `var i = 1;` + // `var j = new Number(1);` + // Neither a nor b can be null, as a !== b and they have the same type. + if (typeof a === 'object') { + a = a.valueOf(); + } + if (typeof b === 'object') { + b = b.valueOf(); } - function compareConstructors (a, b) { - let protoA = Object.getPrototypeOf(a); - let protoB = Object.getPrototypeOf(b); + return a === b; +} - // Comparing constructors is more strict than using `instanceof` - if (a.constructor === b.constructor) { - return true; - } +function compareConstructors (a, b) { + let protoA = Object.getPrototypeOf(a); + let protoB = Object.getPrototypeOf(b); - // Ref #851 - // If the obj prototype descends from a null constructor, treat it - // as a null prototype. - if (protoA && protoA.constructor === null) { - protoA = null; - } - if (protoB && protoB.constructor === null) { - protoB = null; - } - - // Allow objects with no prototype to be equivalent to - // objects with Object as their constructor. - if ( - (protoA === null && protoB === Object.prototype) || - (protoB === null && protoA === Object.prototype) - ) { - return true; - } - - return false; + // Comparing constructors is more strict than using `instanceof` + if (a.constructor === b.constructor) { + return true; } - function getRegExpFlags (regexp) { - return 'flags' in regexp ? regexp.flags : regexp.toString().match(/[gimuy]*$/)[0]; + // Ref #851 + // If the obj prototype descends from a null constructor, treat it + // as a null prototype. + if (protoA && protoA.constructor === null) { + protoA = null; } - - function isContainer (val) { - return ['object', 'array', 'map', 'set'].indexOf(objectType(val)) !== -1; + if (protoB && protoB.constructor === null) { + protoB = null; } - function breadthFirstCompareChild (a, b) { - // If a is a container not reference-equal to b, postpone the comparison to the - // end of the pairs queue -- unless (a, b) has been seen before, in which case skip - // over the pair. - if (a === b) { - return true; - } - if (!isContainer(a)) { - return typeEquiv(a, b); - } - if (pairs.every(function (pair) { - return pair.a !== a || pair.b !== b; - })) { - // Not yet started comparing this pair - pairs.push({ a: a, b: b }); - } + // Allow objects with no prototype to be equivalent to + // objects with Object as their constructor. + if ( + (protoA === null && protoB === Object.prototype) || + (protoB === null && protoA === Object.prototype) + ) { return true; } - const callbacks = { - string: useStrictEquality, - boolean: useStrictEquality, - number: useStrictEquality, - null: useStrictEquality, - undefined: useStrictEquality, - symbol: useStrictEquality, - date: useStrictEquality, + return false; +} + +function getRegExpFlags (regexp) { + return 'flags' in regexp ? regexp.flags : regexp.toString().match(/[gimuy]*$/)[0]; +} - nan: function () { - return true; - }, +function isContainer (val) { + return ['object', 'array', 'map', 'set'].indexOf(objectType(val)) !== -1; +} - regexp: function (a, b) { - return a.source === b.source && +function breadthFirstCompareChild (a, b) { + // If a is a container not reference-equal to b, postpone the comparison to the + // end of the pairs queue -- unless (a, b) has been seen before, in which case skip + // over the pair. + if (a === b) { + return true; + } + if (!isContainer(a)) { + return typeEquiv(a, b); + } + if (pairs.every(function (pair) { + return pair.a !== a || pair.b !== b; + })) { + // Not yet started comparing this pair + pairs.push({ a: a, b: b }); + } + return true; +} + +const callbacks = { + string: useStrictEquality, + boolean: useStrictEquality, + number: useStrictEquality, + null: useStrictEquality, + undefined: useStrictEquality, + symbol: useStrictEquality, + date: useStrictEquality, + + nan: function () { + return true; + }, - // Include flags in the comparison - getRegExpFlags(a) === getRegExpFlags(b); - }, + regexp: function (a, b) { + return a.source === b.source && - // abort (identical references / instance methods were skipped earlier) - function: function () { + // Include flags in the comparison + getRegExpFlags(a) === getRegExpFlags(b); + }, + + // abort (identical references / instance methods were skipped earlier) + function: function () { + return false; + }, + + array: function (a, b) { + const len = a.length; + if (len !== b.length) { + // Safe and faster return false; - }, + } - array: function (a, b) { - const len = a.length; - if (len !== b.length) { - // Safe and faster + for (let i = 0; i < len; i++) { + // Compare non-containers; queue non-reference-equal containers + if (!breadthFirstCompareChild(a[i], b[i])) { return false; } + } + return true; + }, + + // Define sets a and b to be equivalent if for each element aVal in a, there + // is some element bVal in b such that aVal and bVal are equivalent. Element + // repetitions are not counted, so these are equivalent: + // a = new Set( [ {}, [], [] ] ); + // b = new Set( [ {}, {}, [] ] ); + set: function (a, b) { + if (a.size !== b.size) { + // This optimization has certain quirks because of the lack of + // repetition counting. For instance, adding the same + // (reference-identical) element to two equivalent sets can + // make them non-equivalent. + return false; + } - for (let i = 0; i < len; i++) { - // Compare non-containers; queue non-reference-equal containers - if (!breadthFirstCompareChild(a[i], b[i])) { - return false; - } - } - return true; - }, - - // Define sets a and b to be equivalent if for each element aVal in a, there - // is some element bVal in b such that aVal and bVal are equivalent. Element - // repetitions are not counted, so these are equivalent: - // a = new Set( [ {}, [], [] ] ); - // b = new Set( [ {}, {}, [] ] ); - set: function (a, b) { - if (a.size !== b.size) { - // This optimization has certain quirks because of the lack of - // repetition counting. For instance, adding the same - // (reference-identical) element to two equivalent sets can - // make them non-equivalent. - return false; + let outerEq = true; + + a.forEach(function (aVal) { + // Short-circuit if the result is already known. (Using for...of + // with a break clause would be cleaner here, but it would cause + // a syntax error on older JavaScript implementations even if + // Set is unused) + if (!outerEq) { + return; } - let outerEq = true; + let innerEq = false; - a.forEach(function (aVal) { - // Short-circuit if the result is already known. (Using for...of - // with a break clause would be cleaner here, but it would cause - // a syntax error on older JavaScript implementations even if - // Set is unused) - if (!outerEq) { + b.forEach(function (bVal) { + // Likewise, short-circuit if the result is already known + if (innerEq) { return; } - let innerEq = false; - - b.forEach(function (bVal) { - // Likewise, short-circuit if the result is already known - if (innerEq) { - return; - } + // Swap out the global pairs list, as the nested call to + // innerEquiv will clobber its contents + const parentPairs = pairs; + if (innerEquiv(bVal, aVal)) { + innerEq = true; + } - // Swap out the global pairs list, as the nested call to - // innerEquiv will clobber its contents - const parentPairs = pairs; - if (innerEquiv(bVal, aVal)) { - innerEq = true; - } + // Replace the global pairs list + pairs = parentPairs; + }); - // Replace the global pairs list - pairs = parentPairs; - }); + if (!innerEq) { + outerEq = false; + } + }); + + return outerEq; + }, + + // Define maps a and b to be equivalent if for each key-value pair (aKey, aVal) + // in a, there is some key-value pair (bKey, bVal) in b such that + // [ aKey, aVal ] and [ bKey, bVal ] are equivalent. Key repetitions are not + // counted, so these are equivalent: + // a = new Map( [ [ {}, 1 ], [ {}, 1 ], [ [], 1 ] ] ); + // b = new Map( [ [ {}, 1 ], [ [], 1 ], [ [], 1 ] ] ); + map: function (a, b) { + if (a.size !== b.size) { + // This optimization has certain quirks because of the lack of + // repetition counting. For instance, adding the same + // (reference-identical) key-value pair to two equivalent maps + // can make them non-equivalent. + return false; + } - if (!innerEq) { - outerEq = false; - } - }); + let outerEq = true; - return outerEq; - }, - - // Define maps a and b to be equivalent if for each key-value pair (aKey, aVal) - // in a, there is some key-value pair (bKey, bVal) in b such that - // [ aKey, aVal ] and [ bKey, bVal ] are equivalent. Key repetitions are not - // counted, so these are equivalent: - // a = new Map( [ [ {}, 1 ], [ {}, 1 ], [ [], 1 ] ] ); - // b = new Map( [ [ {}, 1 ], [ [], 1 ], [ [], 1 ] ] ); - map: function (a, b) { - if (a.size !== b.size) { - // This optimization has certain quirks because of the lack of - // repetition counting. For instance, adding the same - // (reference-identical) key-value pair to two equivalent maps - // can make them non-equivalent. - return false; + a.forEach(function (aVal, aKey) { + // Short-circuit if the result is already known. (Using for...of + // with a break clause would be cleaner here, but it would cause + // a syntax error on older JavaScript implementations even if + // Map is unused) + if (!outerEq) { + return; } - let outerEq = true; + let innerEq = false; - a.forEach(function (aVal, aKey) { - // Short-circuit if the result is already known. (Using for...of - // with a break clause would be cleaner here, but it would cause - // a syntax error on older JavaScript implementations even if - // Map is unused) - if (!outerEq) { + b.forEach(function (bVal, bKey) { + // Likewise, short-circuit if the result is already known + if (innerEq) { return; } - let innerEq = false; - - b.forEach(function (bVal, bKey) { - // Likewise, short-circuit if the result is already known - if (innerEq) { - return; - } - - // Swap out the global pairs list, as the nested call to - // innerEquiv will clobber its contents - const parentPairs = pairs; - if (innerEquiv([bVal, bKey], [aVal, aKey])) { - innerEq = true; - } - - // Replace the global pairs list - pairs = parentPairs; - }); - - if (!innerEq) { - outerEq = false; + // Swap out the global pairs list, as the nested call to + // innerEquiv will clobber its contents + const parentPairs = pairs; + if (innerEquiv([bVal, bKey], [aVal, aKey])) { + innerEq = true; } - }); - return outerEq; - }, + // Replace the global pairs list + pairs = parentPairs; + }); - object: function (a, b) { - if (compareConstructors(a, b) === false) { - return false; + if (!innerEq) { + outerEq = false; } + }); - const aProperties = []; - const bProperties = []; - - // Be strict: don't ensure hasOwnProperty and go deep - for (const i in a) { - // Collect a's properties - aProperties.push(i); - - // Skip OOP methods that look the same - if ( - a.constructor !== Object && - typeof a.constructor !== 'undefined' && - typeof a[i] === 'function' && - typeof b[i] === 'function' && - a[i].toString() === b[i].toString() - ) { - continue; - } + return outerEq; + }, - // Compare non-containers; queue non-reference-equal containers - if (!breadthFirstCompareChild(a[i], b[i])) { - return false; - } - } + object: function (a, b) { + if (compareConstructors(a, b) === false) { + return false; + } - for (const i in b) { - // Collect b's properties - bProperties.push(i); + const aProperties = []; + const bProperties = []; + + // Be strict: don't ensure hasOwnProperty and go deep + for (const i in a) { + // Collect a's properties + aProperties.push(i); + + // Skip OOP methods that look the same + if ( + a.constructor !== Object && + typeof a.constructor !== 'undefined' && + typeof a[i] === 'function' && + typeof b[i] === 'function' && + a[i].toString() === b[i].toString() + ) { + continue; } - // Ensures identical properties name - return typeEquiv(aProperties.sort(), bProperties.sort()); + // Compare non-containers; queue non-reference-equal containers + if (!breadthFirstCompareChild(a[i], b[i])) { + return false; + } } - }; - - function typeEquiv (a, b) { - const type = objectType(a); - - // Callbacks for containers will append to the pairs queue to achieve breadth-first - // search order. The pairs queue is also used to avoid reprocessing any pair of - // containers that are reference-equal to a previously visited pair (a special case - // this being recursion detection). - // - // Because of this approach, once typeEquiv returns a false value, it should not be - // called again without clearing the pair queue else it may wrongly report a visited - // pair as being equivalent. - return objectType(b) === type && callbacks[type](a, b); - } - function innerEquiv (a, b) { - // We're done when there's nothing more to compare - if (arguments.length < 2) { - return true; + for (const i in b) { + // Collect b's properties + bProperties.push(i); } - // Clear the global pair queue and add the top-level values being compared - pairs = [{ a: a, b: b }]; + // Ensures identical properties name + return typeEquiv(aProperties.sort(), bProperties.sort()); + } +}; + +function typeEquiv (a, b) { + const type = objectType(a); + + // Callbacks for containers will append to the pairs queue to achieve breadth-first + // search order. The pairs queue is also used to avoid reprocessing any pair of + // containers that are reference-equal to a previously visited pair (a special case + // this being recursion detection). + // + // Because of this approach, once typeEquiv returns a false value, it should not be + // called again without clearing the pair queue else it may wrongly report a visited + // pair as being equivalent. + return objectType(b) === type && callbacks[type](a, b); +} + +function innerEquiv (a, b) { + // We're done when there's nothing more to compare + if (arguments.length < 2) { + return true; + } + + // Clear the global pair queue and add the top-level values being compared + pairs = [{ a: a, b: b }]; - for (let i = 0; i < pairs.length; i++) { - const pair = pairs[i]; + for (let i = 0; i < pairs.length; i++) { + const pair = pairs[i]; - // Perform type-specific comparison on any pairs that are not strictly - // equal. For container types, that comparison will postpone comparison - // of any sub-container pair to the end of the pair queue. This gives - // breadth-first search order. It also avoids the reprocessing of - // reference-equal siblings, cousins etc, which can have a significant speed - // impact when comparing a container of small objects each of which has a - // reference to the same (singleton) large object. - if (pair.a !== pair.b && !typeEquiv(pair.a, pair.b)) { - return false; - } + // Perform type-specific comparison on any pairs that are not strictly + // equal. For container types, that comparison will postpone comparison + // of any sub-container pair to the end of the pair queue. This gives + // breadth-first search order. It also avoids the reprocessing of + // reference-equal siblings, cousins etc, which can have a significant speed + // impact when comparing a container of small objects each of which has a + // reference to the same (singleton) large object. + if (pair.a !== pair.b && !typeEquiv(pair.a, pair.b)) { + return false; } - - // ...across all consecutive argument pairs - return arguments.length === 2 || innerEquiv.apply(this, [].slice.call(arguments, 1)); } - return (...args) => { - const result = innerEquiv(...args); + // ...across all consecutive argument pairs + return arguments.length === 2 || innerEquiv.apply(this, [].slice.call(arguments, 1)); +} + +export default function equiv(...args) { + const result = innerEquiv(...args); - // Release any retained objects - pairs.length = 0; - return result; - }; -}()); + // Release any retained objects + pairs.length = 0; + return result; +}; From 5e367cb619cacfea8a03cc027a26842d345dceb6 Mon Sep 17 00:00:00 2001 From: Izel Nakri Date: Tue, 16 Aug 2022 03:47:55 +0200 Subject: [PATCH 02/14] Core: refactor equiv inner functions to ES6 --- src/equiv.js | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/src/equiv.js b/src/equiv.js index e69170cc9..ba284ce80 100644 --- a/src/equiv.js +++ b/src/equiv.js @@ -70,9 +70,7 @@ function breadthFirstCompareChild (a, b) { if (!isContainer(a)) { return typeEquiv(a, b); } - if (pairs.every(function (pair) { - return pair.a !== a || pair.b !== b; - })) { + if (pairs.every((pair) => pair.a !== a || pair.b !== b)) { // Not yet started comparing this pair pairs.push({ a: a, b: b }); } @@ -88,11 +86,11 @@ const callbacks = { symbol: useStrictEquality, date: useStrictEquality, - nan: function () { + nan () { return true; }, - regexp: function (a, b) { + regexp (a, b) { return a.source === b.source && // Include flags in the comparison @@ -100,11 +98,11 @@ const callbacks = { }, // abort (identical references / instance methods were skipped earlier) - function: function () { + function () { return false; }, - array: function (a, b) { + array (a, b) { const len = a.length; if (len !== b.length) { // Safe and faster @@ -125,7 +123,7 @@ const callbacks = { // repetitions are not counted, so these are equivalent: // a = new Set( [ {}, [], [] ] ); // b = new Set( [ {}, {}, [] ] ); - set: function (a, b) { + set (a, b) { if (a.size !== b.size) { // This optimization has certain quirks because of the lack of // repetition counting. For instance, adding the same @@ -136,7 +134,7 @@ const callbacks = { let outerEq = true; - a.forEach(function (aVal) { + a.forEach((aVal) => { // Short-circuit if the result is already known. (Using for...of // with a break clause would be cleaner here, but it would cause // a syntax error on older JavaScript implementations even if @@ -147,7 +145,7 @@ const callbacks = { let innerEq = false; - b.forEach(function (bVal) { + b.forEach((bVal) => { // Likewise, short-circuit if the result is already known if (innerEq) { return; @@ -178,7 +176,7 @@ const callbacks = { // counted, so these are equivalent: // a = new Map( [ [ {}, 1 ], [ {}, 1 ], [ [], 1 ] ] ); // b = new Map( [ [ {}, 1 ], [ [], 1 ], [ [], 1 ] ] ); - map: function (a, b) { + map (a, b) { if (a.size !== b.size) { // This optimization has certain quirks because of the lack of // repetition counting. For instance, adding the same @@ -189,7 +187,7 @@ const callbacks = { let outerEq = true; - a.forEach(function (aVal, aKey) { + a.forEach((aVal, aKey) => { // Short-circuit if the result is already known. (Using for...of // with a break clause would be cleaner here, but it would cause // a syntax error on older JavaScript implementations even if @@ -200,7 +198,7 @@ const callbacks = { let innerEq = false; - b.forEach(function (bVal, bKey) { + b.forEach((bVal, bKey) => { // Likewise, short-circuit if the result is already known if (innerEq) { return; @@ -225,7 +223,7 @@ const callbacks = { return outerEq; }, - object: function (a, b) { + object (a, b) { if (compareConstructors(a, b) === false) { return false; } @@ -307,7 +305,7 @@ function innerEquiv (a, b) { return arguments.length === 2 || innerEquiv.apply(this, [].slice.call(arguments, 1)); } -export default function equiv(...args) { +export default function equiv (...args) { const result = innerEquiv(...args); // Release any retained objects From 5e5ce7dc8b95602fb4029f8569d4adb56d3f8b28 Mon Sep 17 00:00:00 2001 From: Izel Nakri Date: Tue, 16 Aug 2022 03:49:56 +0200 Subject: [PATCH 03/14] Core: refactor equiv for easier readability Small removals/adjustments to make the code more readable. --- src/equiv.js | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/equiv.js b/src/equiv.js index ba284ce80..8f5dfbaaa 100644 --- a/src/equiv.js +++ b/src/equiv.js @@ -66,13 +66,11 @@ function breadthFirstCompareChild (a, b) { // over the pair. if (a === b) { return true; - } - if (!isContainer(a)) { + } else if (!isContainer(a)) { return typeEquiv(a, b); - } - if (pairs.every((pair) => pair.a !== a || pair.b !== b)) { + } else if (pairs.every((pair) => pair.a !== a || pair.b !== b)) { // Not yet started comparing this pair - pairs.push({ a: a, b: b }); + pairs.push({ a, b }); } return true; } @@ -103,13 +101,12 @@ const callbacks = { }, array (a, b) { - const len = a.length; - if (len !== b.length) { + if (a.length !== b.length) { // Safe and faster return false; } - for (let i = 0; i < len; i++) { + for (let i = 0; i < a.length; i++) { // Compare non-containers; queue non-reference-equal containers if (!breadthFirstCompareChild(a[i], b[i])) { return false; @@ -284,7 +281,7 @@ function innerEquiv (a, b) { } // Clear the global pair queue and add the top-level values being compared - pairs = [{ a: a, b: b }]; + pairs = [{ a, b }]; for (let i = 0; i < pairs.length; i++) { const pair = pairs[i]; @@ -311,4 +308,4 @@ export default function equiv (...args) { // Release any retained objects pairs.length = 0; return result; -}; +} From 4944d79214ba4ccaa5e3353a149030999d6260dc Mon Sep 17 00:00:00 2001 From: Izel Nakri Date: Tue, 16 Aug 2022 05:37:27 +0200 Subject: [PATCH 04/14] Core: equiv optimize compareConstructors --- src/equiv.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/equiv.js b/src/equiv.js index 8f5dfbaaa..cc55bae94 100644 --- a/src/equiv.js +++ b/src/equiv.js @@ -22,14 +22,13 @@ function useStrictEquality (a, b) { } function compareConstructors (a, b) { - let protoA = Object.getPrototypeOf(a); - let protoB = Object.getPrototypeOf(b); - - // Comparing constructors is more strict than using `instanceof` if (a.constructor === b.constructor) { return true; } + let protoA = Object.getPrototypeOf(a); + let protoB = Object.getPrototypeOf(b); + // Ref #851 // If the obj prototype descends from a null constructor, treat it // as a null prototype. From dac41dcc8c4ff4057a0fedccf671b2986e2ba953 Mon Sep 17 00:00:00 2001 From: Izel Nakri Date: Tue, 16 Aug 2022 06:01:14 +0200 Subject: [PATCH 05/14] Core: Remove array allocation for innerEquiv --- src/core/utilities.js | 1 + src/equiv.js | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/core/utilities.js b/src/core/utilities.js index d6ff13212..7bdd4dce6 100644 --- a/src/core/utilities.js +++ b/src/core/utilities.js @@ -3,6 +3,7 @@ import Logger from '../logger'; export const toString = Object.prototype.toString; export const hasOwn = Object.prototype.hasOwnProperty; +export const slice = Array.prototype.slice; const nativePerf = getNativePerf(); diff --git a/src/equiv.js b/src/equiv.js index cc55bae94..4060310ac 100644 --- a/src/equiv.js +++ b/src/equiv.js @@ -1,4 +1,4 @@ -import { objectType } from './core/utilities'; +import { objectType, slice } from './core/utilities'; // Value pairs queued for comparison. Used for breadth-first processing order, recursion // detection and avoiding repeated comparison (see below for details). @@ -298,7 +298,7 @@ function innerEquiv (a, b) { } // ...across all consecutive argument pairs - return arguments.length === 2 || innerEquiv.apply(this, [].slice.call(arguments, 1)); + return arguments.length === 2 || innerEquiv.apply(this, slice.call(arguments, 1)); } export default function equiv (...args) { From 7dfa1cbc84ebd96622670c8d960168083137e126 Mon Sep 17 00:00:00 2001 From: Izel Nakri Date: Wed, 17 Aug 2022 00:18:55 +0200 Subject: [PATCH 06/14] Core: Optimize equiv to use a predefined Set --- src/equiv.js | 9 ++++----- src/globals.js | 22 ++++++++++++++++++++++ 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/src/equiv.js b/src/equiv.js index 4060310ac..779e36f7b 100644 --- a/src/equiv.js +++ b/src/equiv.js @@ -1,4 +1,7 @@ import { objectType, slice } from './core/utilities'; +import { StringSet } from './globals'; + +const CONTAINER_TYPES = new StringSet(['object', 'array', 'map', 'set']); // Value pairs queued for comparison. Used for breadth-first processing order, recursion // detection and avoiding repeated comparison (see below for details). @@ -55,17 +58,13 @@ function getRegExpFlags (regexp) { return 'flags' in regexp ? regexp.flags : regexp.toString().match(/[gimuy]*$/)[0]; } -function isContainer (val) { - return ['object', 'array', 'map', 'set'].indexOf(objectType(val)) !== -1; -} - function breadthFirstCompareChild (a, b) { // If a is a container not reference-equal to b, postpone the comparison to the // end of the pairs queue -- unless (a, b) has been seen before, in which case skip // over the pair. if (a === b) { return true; - } else if (!isContainer(a)) { + } else if (!CONTAINER_TYPES.has(objectType(a))) { return typeEquiv(a, b); } else if (pairs.every((pair) => pair.a !== a || pair.b !== b)) { // Not yet started comparing this pair diff --git a/src/globals.js b/src/globals.js index ac7e3dab3..3d7b77d2f 100644 --- a/src/globals.js +++ b/src/globals.js @@ -112,3 +112,25 @@ export const StringMap = typeof g.Map === 'function' && }); } }; + +export const StringSet = g.Set || function (input) { + const set = Object.create(null); + + if (Array.isArray(input)) { + input.forEach(item => { + set[item] = true; + }); + } + + return { + add (value) { + set[value] = true; + }, + has (value) { + return value in set; + }, + get size () { + return Object.keys(set).length; + } + }; +}; From bce6d6683a025ebc8ff0ce53d7a58835e819831c Mon Sep 17 00:00:00 2001 From: Izel Nakri Date: Wed, 17 Aug 2022 03:17:28 +0200 Subject: [PATCH 07/14] Core: Optimize equiv object comparison prop call --- src/equiv.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/equiv.js b/src/equiv.js index 779e36f7b..46a6164a5 100644 --- a/src/equiv.js +++ b/src/equiv.js @@ -253,8 +253,7 @@ const callbacks = { bProperties.push(i); } - // Ensures identical properties name - return typeEquiv(aProperties.sort(), bProperties.sort()); + return callbacks.array(aProperties.sort(), bProperties.sort()); } }; From 0ea1aa86fa15a835feb40efda84ecc06bcb8350f Mon Sep 17 00:00:00 2001 From: Izel Nakri Date: Wed, 17 Aug 2022 05:08:30 +0200 Subject: [PATCH 08/14] Core: Optimize equiv compareConstructors() Makes code more readable and removes variable mutations in compareConstructors(). --- src/equiv.js | 32 ++++++-------------------------- 1 file changed, 6 insertions(+), 26 deletions(-) diff --git a/src/equiv.js b/src/equiv.js index 46a6164a5..2331ac7a9 100644 --- a/src/equiv.js +++ b/src/equiv.js @@ -24,34 +24,14 @@ function useStrictEquality (a, b) { return a === b; } -function compareConstructors (a, b) { - if (a.constructor === b.constructor) { - return true; - } +function getConstructor (obj) { + const proto = Object.getPrototypeOf(obj); - let protoA = Object.getPrototypeOf(a); - let protoB = Object.getPrototypeOf(b); - - // Ref #851 - // If the obj prototype descends from a null constructor, treat it - // as a null prototype. - if (protoA && protoA.constructor === null) { - protoA = null; - } - if (protoB && protoB.constructor === null) { - protoB = null; - } - - // Allow objects with no prototype to be equivalent to - // objects with Object as their constructor. - if ( - (protoA === null && protoB === Object.prototype) || - (protoB === null && protoA === Object.prototype) - ) { - return true; - } + return !proto || proto.constructor === null ? Object : proto.constructor; +} - return false; +function compareConstructors (a, b) { + return getConstructor(a) === getConstructor(b); } function getRegExpFlags (regexp) { From 64409e37fd3d42d9edbeb1f061b88c0ecb2a07d1 Mon Sep 17 00:00:00 2001 From: Izel Nakri Date: Wed, 17 Aug 2022 07:25:29 +0200 Subject: [PATCH 09/14] Core: Make equiv internal pairs state passed in --- src/equiv.js | 41 ++++++++++++++++------------------------- 1 file changed, 16 insertions(+), 25 deletions(-) diff --git a/src/equiv.js b/src/equiv.js index 2331ac7a9..f0f058b92 100644 --- a/src/equiv.js +++ b/src/equiv.js @@ -3,11 +3,6 @@ import { StringSet } from './globals'; const CONTAINER_TYPES = new StringSet(['object', 'array', 'map', 'set']); -// Value pairs queued for comparison. Used for breadth-first processing order, recursion -// detection and avoiding repeated comparison (see below for details). -// Elements are { a: val, b: val }. -let pairs = []; - function useStrictEquality (a, b) { // This only gets called if a and b are not strict equal, and is used to compare on // the primitive values inside object wrappers. For example: @@ -38,14 +33,14 @@ function getRegExpFlags (regexp) { return 'flags' in regexp ? regexp.flags : regexp.toString().match(/[gimuy]*$/)[0]; } -function breadthFirstCompareChild (a, b) { +function breadthFirstCompareChild (a, b, pairs) { // If a is a container not reference-equal to b, postpone the comparison to the // end of the pairs queue -- unless (a, b) has been seen before, in which case skip // over the pair. if (a === b) { return true; } else if (!CONTAINER_TYPES.has(objectType(a))) { - return typeEquiv(a, b); + return typeEquiv(a, b, pairs); } else if (pairs.every((pair) => pair.a !== a || pair.b !== b)) { // Not yet started comparing this pair pairs.push({ a, b }); @@ -78,7 +73,7 @@ const callbacks = { return false; }, - array (a, b) { + array (a, b, pairs) { if (a.length !== b.length) { // Safe and faster return false; @@ -86,7 +81,7 @@ const callbacks = { for (let i = 0; i < a.length; i++) { // Compare non-containers; queue non-reference-equal containers - if (!breadthFirstCompareChild(a[i], b[i])) { + if (!breadthFirstCompareChild(a[i], b[i], pairs)) { return false; } } @@ -98,7 +93,7 @@ const callbacks = { // repetitions are not counted, so these are equivalent: // a = new Set( [ {}, [], [] ] ); // b = new Set( [ {}, {}, [] ] ); - set (a, b) { + set (a, b, pairs) { if (a.size !== b.size) { // This optimization has certain quirks because of the lack of // repetition counting. For instance, adding the same @@ -151,7 +146,7 @@ const callbacks = { // counted, so these are equivalent: // a = new Map( [ [ {}, 1 ], [ {}, 1 ], [ [], 1 ] ] ); // b = new Map( [ [ {}, 1 ], [ [], 1 ], [ [], 1 ] ] ); - map (a, b) { + map (a, b, pairs) { if (a.size !== b.size) { // This optimization has certain quirks because of the lack of // repetition counting. For instance, adding the same @@ -198,7 +193,7 @@ const callbacks = { return outerEq; }, - object (a, b) { + object (a, b, pairs) { if (compareConstructors(a, b) === false) { return false; } @@ -223,7 +218,7 @@ const callbacks = { } // Compare non-containers; queue non-reference-equal containers - if (!breadthFirstCompareChild(a[i], b[i])) { + if (!breadthFirstCompareChild(a[i], b[i], pairs)) { return false; } } @@ -233,11 +228,11 @@ const callbacks = { bProperties.push(i); } - return callbacks.array(aProperties.sort(), bProperties.sort()); + return callbacks.array(aProperties.sort(), bProperties.sort(), pairs); } }; -function typeEquiv (a, b) { +function typeEquiv (a, b, pairs) { const type = objectType(a); // Callbacks for containers will append to the pairs queue to achieve breadth-first @@ -248,7 +243,7 @@ function typeEquiv (a, b) { // Because of this approach, once typeEquiv returns a false value, it should not be // called again without clearing the pair queue else it may wrongly report a visited // pair as being equivalent. - return objectType(b) === type && callbacks[type](a, b); + return objectType(b) === type && callbacks[type](a, b, pairs); } function innerEquiv (a, b) { @@ -257,9 +252,9 @@ function innerEquiv (a, b) { return true; } - // Clear the global pair queue and add the top-level values being compared - pairs = [{ a, b }]; - + // Value pairs queued for comparison. Used for breadth-first processing order, recursion + // detection and avoiding repeated comparison. + let pairs = [{ a, b }]; for (let i = 0; i < pairs.length; i++) { const pair = pairs[i]; @@ -270,7 +265,7 @@ function innerEquiv (a, b) { // reference-equal siblings, cousins etc, which can have a significant speed // impact when comparing a container of small objects each of which has a // reference to the same (singleton) large object. - if (pair.a !== pair.b && !typeEquiv(pair.a, pair.b)) { + if (pair.a !== pair.b && !typeEquiv(pair.a, pair.b, pairs)) { return false; } } @@ -280,9 +275,5 @@ function innerEquiv (a, b) { } export default function equiv (...args) { - const result = innerEquiv(...args); - - // Release any retained objects - pairs.length = 0; - return result; + return innerEquiv(...args); } From f59a216ce015c7a44c9c297d8f309f89ec2d7bc1 Mon Sep 17 00:00:00 2001 From: Izel Nakri Date: Wed, 17 Aug 2022 07:28:44 +0200 Subject: [PATCH 10/14] Core: Remove redundant deepEqual pairs mutations --- src/equiv.js | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/src/equiv.js b/src/equiv.js index f0f058b92..eb45e8ee6 100644 --- a/src/equiv.js +++ b/src/equiv.js @@ -93,7 +93,7 @@ const callbacks = { // repetitions are not counted, so these are equivalent: // a = new Set( [ {}, [], [] ] ); // b = new Set( [ {}, {}, [] ] ); - set (a, b, pairs) { + set (a, b) { if (a.size !== b.size) { // This optimization has certain quirks because of the lack of // repetition counting. For instance, adding the same @@ -121,15 +121,9 @@ const callbacks = { return; } - // Swap out the global pairs list, as the nested call to - // innerEquiv will clobber its contents - const parentPairs = pairs; if (innerEquiv(bVal, aVal)) { innerEq = true; } - - // Replace the global pairs list - pairs = parentPairs; }); if (!innerEq) { @@ -146,7 +140,7 @@ const callbacks = { // counted, so these are equivalent: // a = new Map( [ [ {}, 1 ], [ {}, 1 ], [ [], 1 ] ] ); // b = new Map( [ [ {}, 1 ], [ [], 1 ], [ [], 1 ] ] ); - map (a, b, pairs) { + map (a, b) { if (a.size !== b.size) { // This optimization has certain quirks because of the lack of // repetition counting. For instance, adding the same @@ -174,15 +168,9 @@ const callbacks = { return; } - // Swap out the global pairs list, as the nested call to - // innerEquiv will clobber its contents - const parentPairs = pairs; if (innerEquiv([bVal, bKey], [aVal, aKey])) { innerEq = true; } - - // Replace the global pairs list - pairs = parentPairs; }); if (!innerEq) { From 1c2f1d3cff51edc79a8b9dcc8639692927120d24 Mon Sep 17 00:00:00 2001 From: Izel Nakri Date: Wed, 17 Aug 2022 08:01:13 +0200 Subject: [PATCH 11/14] Core: Optimize Map and Set deepEquals with ES6 --- src/equiv.js | 73 +++++--------------------------------------------- src/globals.js | 3 +++ 2 files changed, 9 insertions(+), 67 deletions(-) diff --git a/src/equiv.js b/src/equiv.js index eb45e8ee6..5541570cb 100644 --- a/src/equiv.js +++ b/src/equiv.js @@ -1,5 +1,5 @@ import { objectType, slice } from './core/utilities'; -import { StringSet } from './globals'; +import { StringSet, ArrayFrom } from './globals'; const CONTAINER_TYPES = new StringSet(['object', 'array', 'map', 'set']); @@ -95,43 +95,12 @@ const callbacks = { // b = new Set( [ {}, {}, [] ] ); set (a, b) { if (a.size !== b.size) { - // This optimization has certain quirks because of the lack of - // repetition counting. For instance, adding the same - // (reference-identical) element to two equivalent sets can - // make them non-equivalent. return false; } - let outerEq = true; + const B_ARRAY = ArrayFrom(b); - a.forEach((aVal) => { - // Short-circuit if the result is already known. (Using for...of - // with a break clause would be cleaner here, but it would cause - // a syntax error on older JavaScript implementations even if - // Set is unused) - if (!outerEq) { - return; - } - - let innerEq = false; - - b.forEach((bVal) => { - // Likewise, short-circuit if the result is already known - if (innerEq) { - return; - } - - if (innerEquiv(bVal, aVal)) { - innerEq = true; - } - }); - - if (!innerEq) { - outerEq = false; - } - }); - - return outerEq; + return ArrayFrom(a).every((aVal) => B_ARRAY.some((bVal) => innerEquiv(bVal, aVal))); }, // Define maps a and b to be equivalent if for each key-value pair (aKey, aVal) @@ -142,43 +111,13 @@ const callbacks = { // b = new Map( [ [ {}, 1 ], [ [], 1 ], [ [], 1 ] ] ); map (a, b) { if (a.size !== b.size) { - // This optimization has certain quirks because of the lack of - // repetition counting. For instance, adding the same - // (reference-identical) key-value pair to two equivalent maps - // can make them non-equivalent. return false; } - let outerEq = true; - - a.forEach((aVal, aKey) => { - // Short-circuit if the result is already known. (Using for...of - // with a break clause would be cleaner here, but it would cause - // a syntax error on older JavaScript implementations even if - // Map is unused) - if (!outerEq) { - return; - } - - let innerEq = false; - - b.forEach((bVal, bKey) => { - // Likewise, short-circuit if the result is already known - if (innerEq) { - return; - } - - if (innerEquiv([bVal, bKey], [aVal, aKey])) { - innerEq = true; - } - }); - - if (!innerEq) { - outerEq = false; - } - }); + const B_ARRAY = ArrayFrom(b); - return outerEq; + return ArrayFrom(a) + .every(([aKey, aVal]) => B_ARRAY.some(([bKey, bVal]) => innerEquiv([bKey, bVal], [aKey, aVal]))); }, object (a, b, pairs) { diff --git a/src/globals.js b/src/globals.js index 3d7b77d2f..acba35ab2 100644 --- a/src/globals.js +++ b/src/globals.js @@ -134,3 +134,6 @@ export const StringSet = g.Set || function (input) { } }; }; + +// eslint-disable-next-line +export const ArrayFrom = Array.from; From a9cd1ab5cbd0ebf89bae36cda056c55f325da5f7 Mon Sep 17 00:00:00 2001 From: Izel Nakri Date: Wed, 17 Aug 2022 08:21:47 +0200 Subject: [PATCH 12/14] Core: Remove redundant comments from deepEqual --- src/equiv.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/equiv.js b/src/equiv.js index 5541570cb..435af45c4 100644 --- a/src/equiv.js +++ b/src/equiv.js @@ -62,10 +62,7 @@ const callbacks = { }, regexp (a, b) { - return a.source === b.source && - - // Include flags in the comparison - getRegExpFlags(a) === getRegExpFlags(b); + return a.source === b.source && getRegExpFlags(a) === getRegExpFlags(b); }, // abort (identical references / instance methods were skipped earlier) @@ -75,7 +72,6 @@ const callbacks = { array (a, b, pairs) { if (a.length !== b.length) { - // Safe and faster return false; } @@ -174,7 +170,6 @@ function typeEquiv (a, b, pairs) { } function innerEquiv (a, b) { - // We're done when there's nothing more to compare if (arguments.length < 2) { return true; } From f8fe9aba55b1e4a60e2a0a0badb883a951c08aa3 Mon Sep 17 00:00:00 2001 From: Izel Nakri Date: Wed, 17 Aug 2022 08:22:33 +0200 Subject: [PATCH 13/14] Core: Optimize array deepEqual with ES6 --- src/equiv.js | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/equiv.js b/src/equiv.js index 435af45c4..49fac63a8 100644 --- a/src/equiv.js +++ b/src/equiv.js @@ -75,13 +75,7 @@ const callbacks = { return false; } - for (let i = 0; i < a.length; i++) { - // Compare non-containers; queue non-reference-equal containers - if (!breadthFirstCompareChild(a[i], b[i], pairs)) { - return false; - } - } - return true; + return a.every((element, index) => breadthFirstCompareChild(element, b[index], pairs)); }, // Define sets a and b to be equivalent if for each element aVal in a, there From af70d5570156b6425b53463e6cb948928983a8ed Mon Sep 17 00:00:00 2001 From: Izel Nakri Date: Wed, 17 Aug 2022 08:23:25 +0200 Subject: [PATCH 14/14] Core: Optimize & benchmark deepEqual object check --- src/equiv.js | 31 +++++++++++++------------------ 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/src/equiv.js b/src/equiv.js index 49fac63a8..bb8e527f2 100644 --- a/src/equiv.js +++ b/src/equiv.js @@ -115,37 +115,32 @@ const callbacks = { return false; } - const aProperties = []; - const bProperties = []; + const aKeys = Object.keys(a); + const bKeys = Object.keys(b); - // Be strict: don't ensure hasOwnProperty and go deep - for (const i in a) { - // Collect a's properties - aProperties.push(i); + if (aKeys.length !== bKeys.length) { + return false; + } - // Skip OOP methods that look the same + for (const key in a) { if ( a.constructor !== Object && typeof a.constructor !== 'undefined' && - typeof a[i] === 'function' && - typeof b[i] === 'function' && - a[i].toString() === b[i].toString() + typeof a[key] === 'function' && + typeof b[key] === 'function' && + a[key].toString() === b[key].toString() ) { continue; + } else if (!(key in b)) { + return false; } - // Compare non-containers; queue non-reference-equal containers - if (!breadthFirstCompareChild(a[i], b[i], pairs)) { + if (!breadthFirstCompareChild(a[key], b[key], pairs)) { return false; } } - for (const i in b) { - // Collect b's properties - bProperties.push(i); - } - - return callbacks.array(aProperties.sort(), bProperties.sort(), pairs); + return true; } };