UNPKG

fast-is-equal

Version:

Blazing-fast equality checks, minus the baggage. A lean, standalone alternative to Lodash's isEqual—because speed matters.

537 lines • 19.4 kB
// Pre-defined constants to avoid repeated string comparisons const TYPEOF_OBJECT = 'object'; const TYPEOF_FUNCTION = 'function'; const TYPEOF_NUMBER = 'number'; const TYPEOF_STRING = 'string'; const TYPEOF_BOOLEAN = 'boolean'; const TYPEOF_SYMBOL = 'symbol'; const TYPEOF_BIGINT = 'bigint'; // Inline NaN check for maximum speed const isNaN = Number.isNaN; // Cache for constructor checks const dateConstructor = Date; const regExpConstructor = RegExp; const mapConstructor = Map; const setConstructor = Set; const arrayBufferConstructor = ArrayBuffer; const promiseConstructor = Promise; const errorConstructor = Error; const dataViewConstructor = DataView; const objectConstructor = Object; export function fastIsEqual(a, b) { // Fast path for strict equality if (a === b) return true; // Handle null/undefined early with single comparison if (a == null || b == null) return false; // Get types once const typeA = typeof a; // Type mismatch = not equal (avoid second typeof if possible) if (typeA === TYPEOF_NUMBER) { // Only NaN pairs survive a failed === between numbers; x !== x is the // cheapest NaN test (no call) return typeof b === TYPEOF_NUMBER && a !== a && b !== b; } // undefined is excluded by the null check above, so any remaining // non-object type (string/boolean/function/symbol/bigint) can't be equal - // we know a !== b from the first check if (typeA !== TYPEOF_OBJECT) return false; // Now check if b is also object if (typeof b !== TYPEOF_OBJECT) return false; // At this point, we know both are objects // Array check using fastest method const aIsArray = Array.isArray(a); if (aIsArray !== Array.isArray(b)) return false; // Constructor check const aCtor = a.constructor; if (aCtor !== b.constructor) return false; // Fast path for arrays - highly optimized if (aIsArray) { const len = a.length; if (len !== b.length) return false; // Empty arrays if (len === 0) return true; // Small arrays - unroll loop with minimal overhead if (len < 8) { // Phase 1 - dense fast loop, no `in` operator at all let i = 0; for (; i < len; i++) { const elemA = a[i]; const elemB = b[i]; // Possible hole or explicit undefined - fall to sparse-aware loop for the rest if (elemA === undefined || elemB === undefined) break; // Fast path for identical elements if (elemA === elemB) continue; // Null check (null still possible - only undefined breaks out) if (elemA == null || elemB == null) return false; // Type check const elemTypeA = typeof elemA; if (elemTypeA !== typeof elemB) return false; // Number special case if (elemTypeA === TYPEOF_NUMBER) { if (!(isNaN(elemA) && isNaN(elemB))) return false; continue; } // Primitive comparison if (elemTypeA !== TYPEOF_OBJECT && elemTypeA !== TYPEOF_FUNCTION) { return false; } // Need deep comparison - visited map allocated lazily on demand if (!deepEqual(elemA, elemB, null)) return false; } if (i === len) return true; // Phase 2 - sparse-aware remainder, baseline per-element logic for (; i < len; i++) { // Sparse array check const hasA = i in a; if (hasA !== (i in b)) return false; if (!hasA) continue; const elemA = a[i]; const elemB = b[i]; // Fast path for identical elements if (elemA === elemB) continue; // Null check if (elemA == null || elemB == null) return false; // Type check const elemTypeA = typeof elemA; if (elemTypeA !== typeof elemB) return false; // Number special case if (elemTypeA === TYPEOF_NUMBER) { if (!(isNaN(elemA) && isNaN(elemB))) return false; continue; } // Primitive comparison if (elemTypeA !== TYPEOF_OBJECT && elemTypeA !== TYPEOF_FUNCTION) { return false; } // Need deep comparison - visited map allocated lazily on demand if (!deepEqual(elemA, elemB, null)) return false; } return true; } // Large arrays - use deep equal return deepEqual(a, b, null); } // Handle built-in types inline for common cases if (aCtor === dateConstructor) { return a.getTime() === b.getTime(); } if (aCtor === regExpConstructor) { return a.source === b.source && a.flags === b.flags; } // For all other objects, use deep comparison return deepEqual(a, b, null); } function deepEqual(valA, valB, visited) { // Fast equality check if (valA === valB) return true; // Null check if (valA == null || valB == null) return false; // Type check const typeA = typeof valA; if (typeA !== typeof valB) return false; // Primitive types if (typeA === TYPEOF_NUMBER) { return isNaN(valA) && isNaN(valB); } if (typeA !== TYPEOF_OBJECT && typeA !== TYPEOF_FUNCTION) { return false; } // Check visited - only when a map has actually been allocated if (visited !== null) { const visitedVal = visited.get(valA); if (visitedVal !== undefined) return visitedVal === valB; if (visited.has(valB)) return false; } // Constructor check const ctorA = valA.constructor; if (ctorA !== valB.constructor) return false; // Fast path for plain objects - the most common recursion target. // ({}).constructor is Object; Object.create(null) has undefined constructor. // Everything else falls through the dispatch chain below. if (ctorA === objectConstructor || ctorA === undefined) { return equalPlainObjects(valA, valB, visited); } // Date - inline comparison if (ctorA === dateConstructor) { return valA.getTime() === valB.getTime(); } // RegExp - inline comparison if (ctorA === regExpConstructor) { return valA.source === valB.source && valA.flags === valB.flags; } // Promise and Error - reference equality only if (ctorA === promiseConstructor || ctorA === errorConstructor) { return false; } // Arrays - optimized if (Array.isArray(valA)) { const len = valA.length; if (len !== valB.length) return false; // Mark visited early - allocate the map lazily on first use if (visited === null) visited = new Map(); visited.set(valA, valB); visited.set(valB, valA); // Empty arrays if (len === 0) return true; // Phase 1 - dense fast loop, no `in` operator at all let i = 0; for (; i < len; i++) { const elemA = valA[i]; const elemB = valB[i]; // Fast path for identical elements if (elemA === elemB) { // Only an undefined pair can hide a hole - disambiguate in phase 2 if (elemA === undefined) break; continue; } // Possible hole or explicit undefined - fall to sparse-aware loop for the rest if (elemA === undefined || elemB === undefined) break; if (!deepEqual(elemA, elemB, visited)) { return false; } } if (i === len) return true; // Phase 2 - sparse-aware remainder, baseline per-element logic for (; i < len; i++) { // Sparse array handling const hasA = i in valA; if (hasA !== (i in valB)) return false; if (!hasA) continue; const elemA = valA[i]; const elemB = valB[i]; if (elemA !== elemB && !deepEqual(elemA, elemB, visited)) { return false; } } return true; } // Map - optimized if (ctorA === mapConstructor) { const mapA = valA; const mapB = valB; if (mapA.size !== mapB.size) return false; // Empty maps if (mapA.size === 0) return true; // Allocate the visited map lazily on first use if (visited === null) visited = new Map(); visited.set(valA, valB); visited.set(valB, valA); // Optimized iteration for (const [key, valueA] of mapA) { // Fast primitive key path const keyType = typeof key; if (keyType !== TYPEOF_OBJECT && keyType !== TYPEOF_FUNCTION) { if (!mapB.has(key)) return false; const valueB = mapB.get(key); if (valueA !== valueB && !deepEqual(valueA, valueB, visited)) { return false; } } else { // Complex key - need full search let found = false; for (const [keyB, valueB] of mapB) { if (deepEqual(key, keyB, visited) && deepEqual(valueA, valueB, visited)) { found = true; break; } } if (!found) return false; } } return true; } // Set - highly optimized if (ctorA === setConstructor) { const setA = valA; const setB = valB; if (setA.size !== setB.size) return false; // Empty sets if (setA.size === 0) return true; // Early visited check - allocate the map lazily on first use if (visited === null) visited = new Map(); visited.set(valA, valB); visited.set(valB, valA); // For equal sets, we can optimize by checking if all primitives exist first let hasPrimitives = false; let hasObjects = false; // First pass - categorize and check primitives for (const val of setA) { const valType = typeof val; if (valType === TYPEOF_OBJECT || valType === TYPEOF_FUNCTION) { hasObjects = true; } else { hasPrimitives = true; if (!setB.has(val)) return false; // Fast fail for primitives } } // If only primitives, we're done if (!hasObjects) return true; // For objects, create arrays for matching const objectsA = []; const objectsB = []; for (const val of setA) { const valType = typeof val; if (valType === TYPEOF_OBJECT || valType === TYPEOF_FUNCTION) { objectsA.push(val); } } for (const val of setB) { const valType = typeof val; if (valType === TYPEOF_OBJECT || valType === TYPEOF_FUNCTION) { objectsB.push(val); } } // Match objects const used = new Uint8Array(objectsB.length); for (const valA of objectsA) { let found = false; for (let j = 0; j < objectsB.length; j++) { if (!used[j]) { const newVisited = visited === null ? new Map() : new Map(visited); if (deepEqual(valA, objectsB[j], newVisited)) { used[j] = 1; found = true; break; } } } if (!found) return false; } return true; } // ArrayBuffer - optimized if (ctorA === arrayBufferConstructor) { const bufA = valA; const bufB = valB; const byteLength = bufA.byteLength; if (byteLength !== bufB.byteLength) return false; const viewA = new Uint8Array(bufA); const viewB = new Uint8Array(bufB); // Unroll loop for better performance on larger buffers let i = 0; const unrollEnd = byteLength - 7; for (; i < unrollEnd; i += 8) { if (viewA[i] !== viewB[i] || viewA[i + 1] !== viewB[i + 1] || viewA[i + 2] !== viewB[i + 2] || viewA[i + 3] !== viewB[i + 3] || viewA[i + 4] !== viewB[i + 4] || viewA[i + 5] !== viewB[i + 5] || viewA[i + 6] !== viewB[i + 6] || viewA[i + 7] !== viewB[i + 7]) { return false; } } // Handle remaining bytes for (; i < byteLength; i++) { if (viewA[i] !== viewB[i]) return false; } return true; } // DataView - optimized if (ctorA === dataViewConstructor) { const viewA = valA; const viewB = valB; if (viewA.byteLength !== viewB.byteLength || viewA.byteOffset !== viewB.byteOffset) { return false; } // Compare the underlying buffer data - hoist byteLength accessor const n = viewA.byteLength; // Large views: wrap in Uint8Array and compare with unrolled loop. // Byte reads through a Uint8Array view of the same buffer region are // value-identical to getUint8 calls, so semantics are unchanged. if (n >= 16) { const bytesA = new Uint8Array(viewA.buffer, viewA.byteOffset, n); const bytesB = new Uint8Array(viewB.buffer, viewB.byteOffset, n); let i = 0; const unrollEnd = n - 7; for (; i < unrollEnd; i += 8) { if (bytesA[i] !== bytesB[i] || bytesA[i + 1] !== bytesB[i + 1] || bytesA[i + 2] !== bytesB[i + 2] || bytesA[i + 3] !== bytesB[i + 3] || bytesA[i + 4] !== bytesB[i + 4] || bytesA[i + 5] !== bytesB[i + 5] || bytesA[i + 6] !== bytesB[i + 6] || bytesA[i + 7] !== bytesB[i + 7]) { return false; } } for (; i < n; i++) { if (bytesA[i] !== bytesB[i]) return false; } return true; } // Small views: per-byte reads avoid two typed-array allocations for (let i = 0; i < n; i++) { if (viewA.getUint8(i) !== viewB.getUint8(i)) return false; } return true; } // TypedArrays if (ArrayBuffer.isView(valA)) { const arrA = valA; const arrB = valB; const len = arrA.length; if (len !== arrB.length) return false; // Small typed arrays if (len < 16) { for (let i = 0; i < len; i++) { if (arrA[i] !== arrB[i]) return false; } return true; } // Large typed arrays - unroll loop let i = 0; const unrollLen = len - 3; for (; i < unrollLen; i += 4) { if (arrA[i] !== arrB[i] || arrA[i + 1] !== arrB[i + 1] || arrA[i + 2] !== arrB[i + 2] || arrA[i + 3] !== arrB[i + 3]) { return false; } } // Handle remaining for (; i < len; i++) { if (arrA[i] !== arrB[i]) return false; } return true; } // Class instances etc. end up in the same plain-object comparison return equalPlainObjects(valA, valB, visited); } // Plain objects - highly optimized function equalPlainObjects(valA, valB, visited) { // Allocate the visited map lazily on first use if (visited === null) visited = new Map(); visited.set(valA, valB); visited.set(valB, valA); // Get keys efficiently const keysA = Object.keys(valA); const keysALen = keysA.length; // Quick length check if (keysALen !== Object.keys(valB).length) return false; // Empty objects - check symbols if (keysALen === 0) { const checkSymbols = Object.getOwnPropertySymbols !== undefined; if (checkSymbols) { const symbolsA = Object.getOwnPropertySymbols(valA); if (symbolsA.length !== Object.getOwnPropertySymbols(valB).length) { return false; } // Check symbol properties for (let i = 0; i < symbolsA.length; i++) { const sym = symbolsA[i]; if (!(sym in valB) || !deepEqual(valA[sym], valB[sym], visited)) { return false; } } } return true; } // Optimized property checking - read values first, defer the "in" probe for (let i = 0; i < keysALen; i++) { const key = keysA[i]; const propA = valA[key]; const propB = valB[key]; // Quick primitive equality check if (propA !== propB) { // Only do deep comparison if needed const propTypeA = typeof propA; if (propTypeA === TYPEOF_OBJECT || propTypeA === TYPEOF_FUNCTION) { if (!deepEqual(propA, propB, visited)) return false; } else if (propTypeA === TYPEOF_NUMBER) { if (!(isNaN(propA) && isNaN(propB))) return false; } else { return false; } } else if (propA === undefined && !(key in valB)) { // Both reads were undefined - distinguish a genuine undefined value // on B from a missing key masquerading as undefined return false; } } // Check for symbols only if likely to have them const checkSymbols = Object.getOwnPropertySymbols !== undefined; if (checkSymbols) { const symbolsA = Object.getOwnPropertySymbols(valA); if (symbolsA.length > 0) { if (symbolsA.length !== Object.getOwnPropertySymbols(valB).length) { return false; } // Check symbol properties for (let i = 0; i < symbolsA.length; i++) { const sym = symbolsA[i]; if (!(sym in valB) || !deepEqual(valA[sym], valB[sym], visited)) { return false; } } } } return true; } //# sourceMappingURL=index.js.map