Object reference vs primitive comparison in JavaScript
Primitives compared by value: `1 === 1` is true. Objects compared by reference: `{a:1} === {a:1}` is false (different references). `===` (strict equality) skips coercion; `==` coerces (`1 == "1"` is true). For deep object equality, use `Object.is` for special cases or a deepEqual utility. `Object.is(NaN, NaN)` is true; `NaN === NaN` is false.
Primitive vs reference
let a = 1, b = 1;
a === b; // true (same value)
let o1 = { x: 1 }, o2 = { x: 1 };
o1 === o2; // false (different references)
let o3 = o1;
o3 === o1; // true (same reference)Primitives (number, string, boolean, null, undefined, symbol, bigint) compare by value. Objects, arrays, functions compare by identity — the same memory reference.
Strict vs loose equality
=== — no coercion. Different types → false. == — coerces. Famous edge cases:
"" == 0; // true (string → number)
"0" == false; // true (both → 0)
null == undefined;// true (special case)
null == 0; // false (special case)
[] == false; // true ([] → "" → 0; false → 0)
[] == ![]; // true (!`` is true; rest is the same as []==true → []==1)Rule of thumb: always use === unless you specifically want null == undefined lumping (a common one-liner: x == null).
Object.is
A "same value" comparison closer to === but with two differences:
Object.is(NaN, NaN); // true (=== returns false)
Object.is(+0, -0); // false (=== returns true)Used internally by React's bailout check (useState skips re-render when next value is Object.is equal).
Deep equality
function deepEqual(a, b) {
if (Object.is(a, b)) return true;
if (typeof a !== "object" || a === null || typeof b !== "object" || b === null) return false;
if (Array.isArray(a) !== Array.isArray(b)) return false;
const ka = Object.keys(a), kb = Object.keys(b);
if (ka.length !== kb.length) return false;
for (const k of ka) if (!deepEqual(a[k], b[k])) return false;
return true;
}Doesn't handle Date / Map / RegExp / cycles — for production use lodash's isEqual or a battle-tested impl.
Comparing references is intentional
const a = { id: 1 };
const b = { id: 1 };
new Set([a, b]).size; // 2 — different references
new Set([a, a]).size; // 1This is feature, not bug — JS uses identity for object keys in Set, Map, WeakMap, etc.
React + reference equality
React bails out renders when state is Object.is equal. If you pass {...obj} every render, React always rerenders consumers — same data, new reference. Memoization (useMemo) and deliberate state updates matter.
Spread + reference
const a = { x: 1 };
const b = { ...a };
a === b; // false (different object)Spread is a shallow copy — top-level reference changes, nested references shared.
Strings
Strings are primitives in JS, compared by value:
"abc" === "abc"; // true
new String("a") === new String("a"); // false (different boxed objects)Interview framing
"Primitives compare by value, objects by reference. === is strict; == coerces and is full of footguns — always prefer === (with one exception: x == null to match both null and undefined). Object.is matches === except it treats NaN as equal to NaN and distinguishes +0 / -0 — React uses it for bailout. For deep object comparison roll your own (handle nested objects, arrays, special types) or use lodash isEqual. The whole reason React state updates need new references is reference equality: mutating an array in place won't trigger re-render."
Follow-up questions
- •Why is === preferable to ==?
- •How does React's bailout use Object.is?
- •When would you implement deep equal?
Common mistakes
- •Using == anywhere except `x == null`.
- •Expecting two object literals to be equal.
- •Mutating state in React without producing a new reference.
Performance considerations
- •Reference compare is O(1). Deep compare can be expensive — avoid in hot paths.
Edge cases
- •NaN === NaN is false; Object.is true.
- •+0 vs -0.
- •Strings vs String objects.
- •Coercion table for ==.
Real-world examples
- •React useMemo dep arrays, Zustand selector equality, useEffect dependency arrays.