UNPKG

fast-is-equal

Version:

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

427 lines • 14.6 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; 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) { // Optimize number comparison - avoid typeof b when possible return typeof b === TYPEOF_NUMBER && isNaN(a) && isNaN(b); } if (typeA === TYPEOF_STRING || typeA === TYPEOF_BOOLEAN || typeA === TYPEOF_FUNCTION || typeA === TYPEOF_SYMBOL || typeA === TYPEOF_BIGINT) { return false; // We know a !== b from first check } // 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) { for (let i = 0; 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 - use minimal visited map if (!deepEqual(elemA, elemB, new Map())) return false; } return true; } // Large arrays - use deep equal return deepEqual(a, b, new Map()); } // 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, new Map()); } 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 - optimized with single lookup 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; // 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 visited.set(valA, valB); visited.set(valB, valA); // Empty arrays if (len === 0) return true; // Optimized loop - check primitives first for early exit for (let i = 0; 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; 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 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 = 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 for (let i = 0; i < viewA.byteLength; i++) { if (viewA.getUint8(i) !== viewB.getUint8(i)) return false; } return true; } // TypedArrays if (ArrayBuffer.isView(valA)) { if (valA.constructor !== valB.constructor) return false; 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; } // Plain objects - highly optimized 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 - batch primitive checks for (let i = 0; i < keysALen; i++) { const key = keysA[i]; // Use in operator for fastest check if (!(key in valB)) return false; 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; } } } // 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