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:
| Comparison | What happens |
|---|---|
| same types | falls through to === |
null == undefined | true — and equal to nothing else |
| number vs string | the string is converted to a number |
| boolean vs anything | the boolean becomes 0 or 1 first |
| object vs primitive | the 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 aboveThe 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 elseIt 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) // falseNaN !== 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) // trueTo 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
Dateobjects compare by time value? - Is
{ a: undefined }equal to{}? - Do
MapandSetcompare 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 withObject.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.
- Challengeeasy
Deep Equal
Every question in the deep-equality list above has to be answered in code, including the NaN row.
- Challengeeasy
Shallow Equal
The one-level version frameworks actually use, which is a different problem from the deep one.
- Quizeasy
Output & Coercion Traps · Output Basics
Coercion snippets — the fastest way to find out whether the table above really stuck.