You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
197 lines
8.1 KiB
197 lines
8.1 KiB
const { toString, hasOwnProperty } = Object.prototype; |
|
const fnToStr = Function.prototype.toString; |
|
const previousComparisons = new Map(); |
|
/** |
|
* Performs a deep equality check on two JavaScript values, tolerating cycles. |
|
*/ |
|
export function equal(a, b) { |
|
try { |
|
return check(a, b); |
|
} |
|
finally { |
|
previousComparisons.clear(); |
|
} |
|
} |
|
// Allow default imports as well. |
|
export default equal; |
|
function check(a, b) { |
|
// If the two values are strictly equal, our job is easy. |
|
if (a === b) { |
|
return true; |
|
} |
|
// Object.prototype.toString returns a representation of the runtime type of |
|
// the given value that is considerably more precise than typeof. |
|
const aTag = toString.call(a); |
|
const bTag = toString.call(b); |
|
// If the runtime types of a and b are different, they could maybe be equal |
|
// under some interpretation of equality, but for simplicity and performance |
|
// we just return false instead. |
|
if (aTag !== bTag) { |
|
return false; |
|
} |
|
switch (aTag) { |
|
case '[object Array]': |
|
// Arrays are a lot like other objects, but we can cheaply compare their |
|
// lengths as a short-cut before comparing their elements. |
|
if (a.length !== b.length) |
|
return false; |
|
// Fall through to object case... |
|
case '[object Object]': { |
|
if (previouslyCompared(a, b)) |
|
return true; |
|
const aKeys = definedKeys(a); |
|
const bKeys = definedKeys(b); |
|
// If `a` and `b` have a different number of enumerable keys, they |
|
// must be different. |
|
const keyCount = aKeys.length; |
|
if (keyCount !== bKeys.length) |
|
return false; |
|
// Now make sure they have the same keys. |
|
for (let k = 0; k < keyCount; ++k) { |
|
if (!hasOwnProperty.call(b, aKeys[k])) { |
|
return false; |
|
} |
|
} |
|
// Finally, check deep equality of all child properties. |
|
for (let k = 0; k < keyCount; ++k) { |
|
const key = aKeys[k]; |
|
if (!check(a[key], b[key])) { |
|
return false; |
|
} |
|
} |
|
return true; |
|
} |
|
case '[object Error]': |
|
return a.name === b.name && a.message === b.message; |
|
case '[object Number]': |
|
// Handle NaN, which is !== itself. |
|
if (a !== a) |
|
return b !== b; |
|
// Fall through to shared +a === +b case... |
|
case '[object Boolean]': |
|
case '[object Date]': |
|
return +a === +b; |
|
case '[object RegExp]': |
|
case '[object String]': |
|
return a == `${b}`; |
|
case '[object Map]': |
|
case '[object Set]': { |
|
if (a.size !== b.size) |
|
return false; |
|
if (previouslyCompared(a, b)) |
|
return true; |
|
const aIterator = a.entries(); |
|
const isMap = aTag === '[object Map]'; |
|
while (true) { |
|
const info = aIterator.next(); |
|
if (info.done) |
|
break; |
|
// If a instanceof Set, aValue === aKey. |
|
const [aKey, aValue] = info.value; |
|
// So this works the same way for both Set and Map. |
|
if (!b.has(aKey)) { |
|
return false; |
|
} |
|
// However, we care about deep equality of values only when dealing |
|
// with Map structures. |
|
if (isMap && !check(aValue, b.get(aKey))) { |
|
return false; |
|
} |
|
} |
|
return true; |
|
} |
|
case '[object Uint16Array]': |
|
case '[object Uint8Array]': // Buffer, in Node.js. |
|
case '[object Uint32Array]': |
|
case '[object Int32Array]': |
|
case '[object Int8Array]': |
|
case '[object Int16Array]': |
|
case '[object ArrayBuffer]': |
|
// DataView doesn't need these conversions, but the equality check is |
|
// otherwise the same. |
|
a = new Uint8Array(a); |
|
b = new Uint8Array(b); |
|
// Fall through... |
|
case '[object DataView]': { |
|
let len = a.byteLength; |
|
if (len === b.byteLength) { |
|
while (len-- && a[len] === b[len]) { |
|
// Keep looping as long as the bytes are equal. |
|
} |
|
} |
|
return len === -1; |
|
} |
|
case '[object AsyncFunction]': |
|
case '[object GeneratorFunction]': |
|
case '[object AsyncGeneratorFunction]': |
|
case '[object Function]': { |
|
const aCode = fnToStr.call(a); |
|
if (aCode !== fnToStr.call(b)) { |
|
return false; |
|
} |
|
// We consider non-native functions equal if they have the same code |
|
// (native functions require === because their code is censored). |
|
// Note that this behavior is not entirely sound, since !== function |
|
// objects with the same code can behave differently depending on |
|
// their closure scope. However, any function can behave differently |
|
// depending on the values of its input arguments (including this) |
|
// and its calling context (including its closure scope), even |
|
// though the function object is === to itself; and it is entirely |
|
// possible for functions that are not === to behave exactly the |
|
// same under all conceivable circumstances. Because none of these |
|
// factors are statically decidable in JavaScript, JS function |
|
// equality is not well-defined. This ambiguity allows us to |
|
// consider the best possible heuristic among various imperfect |
|
// options, and equating non-native functions that have the same |
|
// code has enormous practical benefits, such as when comparing |
|
// functions that are repeatedly passed as fresh function |
|
// expressions within objects that are otherwise deeply equal. Since |
|
// any function created from the same syntactic expression (in the |
|
// same code location) will always stringify to the same code |
|
// according to fnToStr.call, we can reasonably expect these |
|
// repeatedly passed function expressions to have the same code, and |
|
// thus behave "the same" (with all the caveats mentioned above), |
|
// even though the runtime function objects are !== to one another. |
|
return !endsWith(aCode, nativeCodeSuffix); |
|
} |
|
} |
|
// Otherwise the values are not equal. |
|
return false; |
|
} |
|
function definedKeys(obj) { |
|
// Remember that the second argument to Array.prototype.filter will be |
|
// used as `this` within the callback function. |
|
return Object.keys(obj).filter(isDefinedKey, obj); |
|
} |
|
function isDefinedKey(key) { |
|
return this[key] !== void 0; |
|
} |
|
const nativeCodeSuffix = "{ [native code] }"; |
|
function endsWith(full, suffix) { |
|
const fromIndex = full.length - suffix.length; |
|
return fromIndex >= 0 && |
|
full.indexOf(suffix, fromIndex) === fromIndex; |
|
} |
|
function previouslyCompared(a, b) { |
|
// Though cyclic references can make an object graph appear infinite from the |
|
// perspective of a depth-first traversal, the graph still contains a finite |
|
// number of distinct object references. We use the previousComparisons cache |
|
// to avoid comparing the same pair of object references more than once, which |
|
// guarantees termination (even if we end up comparing every object in one |
|
// graph to every object in the other graph, which is extremely unlikely), |
|
// while still allowing weird isomorphic structures (like rings with different |
|
// lengths) a chance to pass the equality test. |
|
let bSet = previousComparisons.get(a); |
|
if (bSet) { |
|
// Return true here because we can be sure false will be returned somewhere |
|
// else if the objects are not equivalent. |
|
if (bSet.has(b)) |
|
return true; |
|
} |
|
else { |
|
previousComparisons.set(a, bSet = new Set); |
|
} |
|
bSet.add(b); |
|
return false; |
|
} |
|
//# sourceMappingURL=index.js.map
|