본문으로 건너뛰기

stableEquals

Performs robust deep equality comparison with advanced type support and circular reference handling. Enhanced version of deep equality comparison that handles complex data structures including circular references, typed arrays, dates, regular expressions, and sparse arrays. Uses optimized algorithms with visited object tracking to prevent infinite loops while maintaining high performance for nested structures. Key Differences from equals:

  • Circular Reference Safe: Uses Map-based tracking to handle self-referencing objects
  • Advanced Type Support: Proper comparison for Date, RegExp, TypedArrays, ArrayBuffers
  • Sparse Array Handling: Correctly distinguishes sparse arrays from dense ones
  • Symbol Property Support: Compares symbol-keyed properties using Reflect.ownKeys
  • Memory Safety: No risk of stack overflow with deeply nested circular structures
  • Performance: ~2x slower than equals but handles much more complex scenarios When to Use stableEquals vs equals:
// Use equals() for:
- Simple objects without circular references
- Performance-critical code paths
- Basic comparison needs
- Objects with only string/number keys
// Use stableEquals() for:
- Objects with potential circular references
- Complex data with Date, RegExp, TypedArrays
- Sparse arrays or arrays with holes
- Objects with symbol properties
- Maximum reliability over performance

Signature

const stableEquals: (left: unknown, right: unknown, omit?: PropertyKey[]) => boolean

Parameters

NameTypeDescription
left-First value to compare
right-Second value to compare
omit-Array of property keys to exclude from comparison (optional)

Returns

true if values are deeply equal considering all supported types, false otherwise

Examples

Basic equality comparison

import { stableEquals } from '@winglet/common-utils';

// Primitive values with NaN handling
console.log(stableEquals(42, 42)); // true
console.log(stableEquals('hello', 'hello')); // true
console.log(stableEquals(NaN, NaN)); // true (unlike === operator)
console.log(stableEquals(null, undefined)); // false

// Object comparison
const obj1 = { name: 'John', age: 30 };
const obj2 = { name: 'John', age: 30 };
console.log(stableEquals(obj1, obj2)); // true
console.log(obj1 === obj2); // false (different references)

Advanced type support

// Date objects
const date1 = new Date('2023-01-01T12:00:00Z');
const date2 = new Date('2023-01-01T12:00:00Z');
const date3 = new Date('2023-01-01T13:00:00Z');
console.log(stableEquals(date1, date2)); // true (same timestamp)
console.log(stableEquals(date1, date3)); // false (different timestamp)

// RegExp objects
const regex1 = /test/gi;
const regex2 = /test/gi;
const regex3 = /test/g; // different flags
console.log(stableEquals(regex1, regex2)); // true (same pattern and flags)
console.log(stableEquals(regex1, regex3)); // false (different flags)

// TypedArrays and ArrayBuffers
const uint8_1 = new Uint8Array([1, 2, 3, 4]);
const uint8_2 = new Uint8Array([1, 2, 3, 4]);
const uint8_3 = new Uint8Array([1, 2, 3, 5]);
console.log(stableEquals(uint8_1, uint8_2)); // true (same content)
console.log(stableEquals(uint8_1, uint8_3)); // false (different content)

Circular reference handling

// Objects with circular references
const circular1: any = { name: 'parent' };
circular1.self = circular1;
circular1.child = { parent: circular1, name: 'child' };

const circular2: any = { name: 'parent' };
circular2.self = circular2;
circular2.child = { parent: circular2, name: 'child' };

// stableEquals can handle circular references
console.log(stableEquals(circular1, circular2)); // true

// Different circular structures
const different: any = { name: 'different' };
different.self = different;
console.log(stableEquals(circular1, different)); // false

Sparse array comparison

// Sparse arrays (arrays with holes)
const sparse1 = new Array(5);
sparse1[0] = 'first';
sparse1[4] = 'last';
// sparse1[1], sparse1[2], sparse1[3] are holes

const sparse2 = new Array(5);
sparse2[0] = 'first';
sparse2[4] = 'last';

const dense = ['first', undefined, undefined, undefined, 'last'];

console.log(stableEquals(sparse1, sparse2)); // true (same sparse structure)
console.log(stableEquals(sparse1, dense)); // false (sparse vs dense)

// Regular arrays
const arr1 = [1, 2, { nested: true }, [4, 5]];
const arr2 = [1, 2, { nested: true }, [4, 5]];
console.log(stableEquals(arr1, arr2)); // true

Property exclusion

// Exclude specific properties from comparison
const user1 = {
id: 1,
name: 'Alice',
lastLogin: '2023-01-01T10:00:00Z',
sessionId: 'abc123'
};

const user2 = {
id: 1,
name: 'Alice',
lastLogin: '2023-01-01T11:00:00Z', // Different
sessionId: 'xyz789' // Different
};

// Compare excluding volatile fields
console.log(stableEquals(user1, user2)); // false
console.log(stableEquals(user1, user2, ['lastLogin', 'sessionId'])); // true

Complex nested structures

// Deeply nested objects with mixed types
const complex1 = {
metadata: {
created: new Date('2023-01-01'),
pattern: /\w+/g,
tags: ['important', 'user-data']
},
data: {
users: [
{ id: 1, profile: { active: true, scores: new Float32Array([95.5, 87.2]) } },
{ id: 2, profile: { active: false, scores: new Float32Array([78.1, 92.3]) } }
],
summary: {
total: 2,
active: 1,
averageScore: 88.275
}
}
};

const complex2 = {
metadata: {
created: new Date('2023-01-01'),
pattern: /\w+/g,
tags: ['important', 'user-data']
},
data: {
users: [
{ id: 1, profile: { active: true, scores: new Float32Array([95.5, 87.2]) } },
{ id: 2, profile: { active: false, scores: new Float32Array([78.1, 92.3]) } }
],
summary: {
total: 2,
active: 1,
averageScore: 88.275
}
}
};

console.log(stableEquals(complex1, complex2)); // true

Performance with large structures

// Large nested structures
const createLargeStructure = (depth: number, breadth: number) => {
const obj: any = {};

for (let i = 0; i < breadth; i++) {
if (depth > 0) {
obj[`branch_${i}`] = createLargeStructure(depth - 1, breadth);
} else {
obj[`leaf_${i}`] = `value_${i}`;
}
}

return obj;
};

const large1 = createLargeStructure(5, 10); // Deep structure
const large2 = createLargeStructure(5, 10); // Identical structure

console.time('stableEquals-large');
const isEqual = stableEquals(large1, large2);
console.timeEnd('stableEquals-large');
console.log(isEqual); // true

Symbol properties and edge cases

// Objects with symbol properties
const sym1 = Symbol('test');
const sym2 = Symbol('test'); // Different symbol

const obj1 = { regular: 'prop', [sym1]: 'symbol value' };
const obj2 = { regular: 'prop', [sym1]: 'symbol value' };
const obj3 = { regular: 'prop', [sym2]: 'symbol value' };

console.log(stableEquals(obj1, obj2)); // true (same symbol reference)
console.log(stableEquals(obj1, obj3)); // false (different symbol)

// Mixed types comparison
console.log(stableEquals({}, [])); // false (object vs array)
console.log(stableEquals(new Date(), {})); // false (Date vs plain object)
console.log(stableEquals(/regex/, {})); // false (RegExp vs plain object)

Playground

import { stableEquals } from '@winglet/common-utils';

// Primitive values with NaN handling
console.log(stableEquals(42, 42)); // true
console.log(stableEquals('hello', 'hello')); // true
console.log(stableEquals(NaN, NaN)); // true (unlike === operator)
console.log(stableEquals(null, undefined)); // false

// Object comparison
const obj1 = { name: 'John', age: 30 };
const obj2 = { name: 'John', age: 30 };
console.log(stableEquals(obj1, obj2)); // true
console.log(obj1 === obj2); // false (different references)

Notes

Enhanced Features over Basic Equals:

  • Circular Reference Detection: Prevents infinite loops with visited object tracking
  • Advanced Type Support: Handles Date, RegExp, TypedArray, ArrayBuffer, DataView
  • Sparse Array Handling: Correctly compares arrays with holes vs dense arrays
  • Symbol Property Support: Compares symbol-keyed properties using Reflect.ownKeys
  • Memory Optimization: Uses WeakMap for efficient visited object tracking

Comparison Strategy:

  • Primitives: Standard equality with special NaN handling (NaN === NaN)
  • Dates: Compares internal timestamp values using getTime()
  • RegExp: Compares source pattern and flags for complete equality
  • TypedArrays: Byte-by-byte comparison using DataView for accuracy
  • Objects: Deep recursive comparison with circular reference protection
  • Arrays: Index-by-index comparison with sparse array hole detection

Performance Characteristics:

  • Time Complexity: O(n) where n is total number of properties/elements
  • Space Complexity: O(d + c) where d is depth, c is number of circular references
  • Optimization: Early termination, reference equality check, visited object caching
  • Memory Efficient: WeakMap-based visited tracking with automatic cleanup

Circular Reference Handling:

  • Detection: Uses Map to track visited object pairs during traversal
  • Prevention: Returns true if circular reference is detected (assumes structural equality)
  • Bidirectional: Tracks both left->right and right->left object relationships
  • Memory Safe: No memory leaks from circular reference tracking

Supported Data Types:

  • Primitives: string, number, boolean, null, undefined, symbol, bigint
  • Objects: Plain objects, arrays (including sparse), functions
  • Built-ins: Date, RegExp, Error, Map, Set (reference equality)
  • Binary Data: ArrayBuffer, SharedArrayBuffer, TypedArrays, DataView
  • Custom Objects: Objects with custom prototypes

Use Cases:

  • Testing Frameworks: Deep equality assertions for complex data structures
  • State Management: Comparing application state with circular references
  • Caching Systems: Determining if cached data matches current data
  • Data Validation: Verifying API responses against expected structures
  • Change Detection: Identifying modifications in reactive systems
  • Configuration Comparison: Comparing complex configuration objects

Limitations:

  • Set/Map Comparison: Currently uses reference equality (could be enhanced)
  • Function Comparison: Compares by reference only, not implementation
  • Custom Class Instances: May not handle specialized comparison logic
  • Performance on Extremely Large Objects: May be slower than simple reference checks
  • Prototype Chain: Only compares own properties, ignores inherited properties