Lesson 77 of 80
`===` compares references, not contents. Writing your own deepEqual — and knowing why JSON.stringify comparisons quietly lie to you — is the real skill.
15 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
This topic is about judgment and practical tradeoffs more than a single right answer — a mock interview is the better way to test yourself on it.
Be interviewed on this →Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
{ a: 1 } === { a: 1 } is false — two different objects that happen to look alike, compared by reference, not content. Deep equality means: walk both values recursively and ask "do these have the same shape and the same values at every level," ignoring the fact that they're different objects in memory. You need this constantly — comparing Redux/Zustand state slices, deciding whether a memoized component should re-render, testing that an API response matches an expected fixture.
function deepEqual(a, b) {
if (Object.is(a, b)) return true; // handles primitives, NaN, same reference
if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) {
return false; // one is a primitive/null, the other isn't — can't be equal
}
if (Array.isArray(a) !== Array.isArray(b)) return false;
if (a instanceof Date && b instanceof Date) {
return a.getTime() === b.getTime();
}
const keysA = Object.keys(a);
const keysB = Object.keys(b);
if (keysA.length !== keysB.length) return false;
return keysA.every(
(key) => Object.prototype.hasOwnProperty.call(b, key) && deepEqual(a[key], b[key])
);
}
deepEqual({ a: [1, { b: 2 }] }, { a: [1, { b: 2 }] }); // true
deepEqual({ a: 1 }, { a: 1, b: undefined }); // false — extra key, even if undefined
Object.is up front is doing more than === would: it correctly treats NaN as equal to itself (=== doesn't) and correctly treats +0 and -0 as different (most naive implementations get one of these wrong). The key-count check plus every is what makes this recursive rather than shallow — deepEqual(a[key], b[key]) calls itself for nested objects and bottoms out at the primitive case.
JSON.stringify(a) === JSON.stringify(b) is not deep equality. Key order changes the string ({a:1,b:2} vs {b:2,a:1} stringify differently despite being equal objects), and it silently drops undefined values, functions, and Symbol keys — two objects that differ only in those can stringify identically or differently in ways that don't track real equality.structuredClone solves copying, not comparing. It's for making a real deep copy, not for testing equality — people sometimes reach for structuredClone(a) then compare, which is strictly more expensive than just writing deepEqual and doesn't handle functions (it throws on them) or getters.NaN and -0 are the edge cases that expose a naive ===-based implementation. NaN === NaN is false and +0 === -0 is true, both backwards from what "these values are the same" usually means — Object.is gets both right, which is exactly why it belongs at the top of deepEqual instead of ===.This lesson leans on the same object-walking instinct as structuredClone and the JSON deep-dive — all three are answers to "objects don't compare or copy by value, so what do you do." Deep equality is the comparison half of that problem; structured cloning is the copying half.