codejump
Academy

Equality: ==, ===, Object.is and deep equality

What loose equality really does, the two cases where === is not what you want, and why comparing objects almost never means what people expect.

Updated

=== compares without converting

Strict equality asks two questions: same type, and same value. If the types differ the answer is false and nothing else happens.

For objects, "same value" means same reference. Two objects with identical contents are not equal, and that is not a quirk — it is the only definition the language can apply without guessing what you meant by equal.

{ a: 1 } === { a: 1 }      // false — two different objects
[1, 2] === [1, 2]          // false
const x = { a: 1 };
x === x                    // true

== converts first, by rules worth knowing once

Loose equality coerces before comparing. The algorithm is smaller than its reputation:

ComparisonWhat happens
same typesfalls through to ===
null == undefinedtrue — and equal to nothing else
number vs stringthe string is converted to a number
boolean vs anythingthe boolean becomes 0 or 1 first
object vs primitivethe object is converted to a primitive

That table explains the famous results:

'' == 0            // true  — '' becomes 0
'0' == 0           // true  — '0' becomes 0
'' == '0'          // false — both strings, no conversion
null == 0          // false — null converts to nothing
null == undefined  // true  — the special case
[] == false        // true  — false→0, []→''→0
[] == ![]          // true  — ![] is false, then the line above

The last one is a party trick, not a hazard. The hazard is the third line: == is not transitive, so you cannot reason about it locally.

When == is defensible

Exactly one idiom is genuinely useful:

if (value == null) { ... }   // true for null AND undefined, nothing else

It is shorter than value === null || value === undefined, and it is what ?? and ?. check internally. Some style guides allow it and ban every other ==. Everywhere else, the cost of remembering the table outweighs the saved keystroke.

Object.is — for the two cases === gets wrong

NaN === NaN            // false
Object.is(NaN, NaN)    // true

0 === -0               // true
Object.is(0, -0)       // false

NaN !== NaN is required by the floating-point standard — NaN means "not a number", and two non-numbers have no reason to be the same one. Practically it means you cannot find it with indexOf (which uses ===), but you can with includes (which uses Object.is semantics):

[NaN].indexOf(NaN)     // -1
[NaN].includes(NaN)    // true

To test for it directly, use Number.isNaN(x) — never the global isNaN, which coerces first and calls 'hello' a NaN.

The -0 case matters far less, until you divide by it and get -Infinity.

Deep equality, and why frameworks avoid it

When people say two objects "are equal" they usually mean structurally equal, and nothing in the language gives them that. You write it, or you import it — and it costs O(n) in the size of the structure, with real decisions to make:

  • Do Date objects compare by time value?
  • Is { a: undefined } equal to {}?
  • Do Map and Set compare by contents, and does order count?
  • What happens on a circular reference?

This is precisely why React, Vue and every memoisation layer compare references, not contents. Reference comparison is O(1) and always correct about "is this the same object". It is the reason immutable updates matter: replacing an object is how you signal a change to a system that only looks at identity.

shallowEqual — comparing one level with Object.is — is the compromise the ecosystem settled on. Cheap enough to run on every render, deep enough to catch the case where a new object holds the same values.

Now practice it

Reading this page is the cheap half. These are the exercises that make you use it.