import { isPlainObject, isPromiseLike, sameValueZeroEqual } from './utils'; import type { CreateComparatorCreatorOptions, EqualityComparator, } from '../index.d'; const ARGUMENTS_TAG = '[object Arguments]'; const BOOLEAN_TAG = '[object Boolean]'; const DATE_TAG = '[object Date]'; const REG_EXP_TAG = '[object RegExp]'; const MAP_TAG = '[object Map]'; const NUMBER_TAG = '[object Number]'; const OBJECT_TAG = '[object Object]'; const SET_TAG = '[object Set]'; const STRING_TAG = '[object String]'; const { toString } = Object.prototype; export function createComparator({ areArraysEqual, areDatesEqual, areMapsEqual, areObjectsEqual, areRegExpsEqual, areSetsEqual, createIsNestedEqual, }: CreateComparatorCreatorOptions): EqualityComparator { const isEqual = createIsNestedEqual(comparator as EqualityComparator); /** * compare the value of the two objects and return true if they are equivalent in values */ function comparator(a: any, b: any, meta: Meta): boolean { // If the items are strictly equal, no need to do a value comparison. if (a === b) { return true; } // If the items are not non-nullish objects, then the only possibility // of them being equal but not strictly is if they are both `NaN`. Since // `NaN` is uniquely not equal to itself, we can use self-comparison of // both objects, which is faster than `isNaN()`. if (!a || !b || typeof a !== 'object' || typeof b !== 'object') { return a !== a && b !== b; } // Checks are listed in order of commonality of use-case: // 1. Common complex object types (plain object, array) // 2. Common data values (date, regexp) // 3. Less-common complex object types (map, set) // 4. Less-common data values (promise, primitive wrappers) // Inherently this is both subjective and assumptive, however // when reviewing comparable libraries in the wild this order // appears to be generally consistent. // `isPlainObject` only checks against the object's own realm. Cross-realm // comparisons are rare, and will be handled in the ultimate fallback, so // we can avoid the `toString.call()` cost unless necessary. if (isPlainObject(a) && isPlainObject(b)) { return areObjectsEqual(a, b, isEqual, meta); } // `isArray()` works on subclasses and is cross-realm, so we can again avoid // the `toString.call()` cost unless necessary by just checking if either // and then both are arrays. const aArray = Array.isArray(a); const bArray = Array.isArray(b); if (aArray || bArray) { return aArray === bArray && areArraysEqual(a, b, isEqual, meta); } // Since this is a custom object, use the classic `toString.call()` to get its // type. This is reasonably performant in modern environments like v8 and // SpiderMonkey, and allows for cross-realm comparison when other checks like // `instanceof` do not. const aTag = toString.call(a); if (aTag !== toString.call(b)) { return false; } if (aTag === DATE_TAG) { // `getTime()` showed better results compared to alternatives like `valueOf()` // or the unary `+` operator. return areDatesEqual(a, b, isEqual, meta); } if (aTag === REG_EXP_TAG) { return areRegExpsEqual(a, b, isEqual, meta); } if (aTag === MAP_TAG) { return areMapsEqual(a, b, isEqual, meta); } if (aTag === SET_TAG) { return areSetsEqual(a, b, isEqual, meta); } // If a simple object tag, then we can prioritize a simple object comparison because // it is likely a custom class. If an arguments tag, it should be treated as a standard // object. if (aTag === OBJECT_TAG || aTag === ARGUMENTS_TAG) { // The exception for value comparison is `Promise`-like contracts. These should be // treated the same as standard `Promise` objects, which means strict equality. return isPromiseLike(a) || isPromiseLike(b) ? false : areObjectsEqual(a, b, isEqual, meta); } // As the penultimate fallback, check if the values passed are primitive wrappers. This // is very rare in modern JS, which is why it is deprioritized compared to all other object // types. if (aTag === BOOLEAN_TAG || aTag === NUMBER_TAG || aTag === STRING_TAG) { return sameValueZeroEqual(a.valueOf(), b.valueOf()); } // If not matching any tags that require a specific type of comparison, then we hard-code false because // the only thing remaining is strict equality, which has already been compared. This is for a few reasons: // - Certain types that cannot be introspected (e.g., `WeakMap`). For these types, this is the only // comparison that can be made. // - For types that can be introspected, but rarely have requirements to be compared // (`ArrayBuffer`, `DataView`, etc.), the cost is avoided to prioritize the common // use-cases (may be included in a future release, if requested enough). // - For types that can be introspected but do not have an objective definition of what // equality is (`Error`, etc.), the subjective decision is to be conservative and strictly compare. // In all cases, these decisions should be reevaluated based on changes to the language and // common development practices. return false; } return comparator as EqualityComparator; }