Objects are compared by reference: {} === {} is false, and Object.is({}, {}) is false too. JSON.stringify(a) === JSON.stringify(b) is a trap, not a substitute: it depends on key order, turns NaN into null, drops undefined and functions, and throws on cycles and BigInt.
Deep equality compares structure. The algorithm:
- If
Object.is(a, b), they are equal.Object.isisSameValue, soNaNequalsNaN, and0differs from-0. - If either is a non-object (or
null), they differ. - If their tags or prototypes differ, they differ.
- Special types need their own rule:
DatecomparesgetTime(). - Otherwise compare own enumerable keys: same count, every key present on both, and values equal recursively. Key order doesn't matter.
- Cycles: remember which
(a, b)pairs are being compared. If you meet a pair again, assume equal, which stops the infinite recursion.
Every step is a policy decision: whether -0 equals 0, whether { a: undefined } equals {}, whether different classes with the same fields are equal, whether symbol keys and non-enumerable properties count, and how Map, Set, RegExp and typed arrays compare.
In Node, util.isDeepStrictEqual makes one set of choices: same prototypes, NaN equal, -0 differs, Map and Set compared by content. Outside Node, use a maintained library for production code.
The implementation below is deliberately limited to plain objects, arrays, class instances and Date.