Equality
Mental model: === asks "same type and same value". == asks the same question after converting, which is why it needs a rulebook.
Level: beginner · about 11 minutes
console.log(1 === '1'); // false different types, no conversion
console.log(1 == '1'); // true the string is converted to a number
console.log(null === undefined); // false two distinct values
console.log(null == undefined); // true a hard-coded special caseSame four values, two operators, four different answers.
=== is strict equality: if the types differ the answer is false, full stop. == is loose equality: when the types differ it converts first, following a fixed sequence of steps. Neither is broken. One of them just does more work on your behalf than you can usually keep track of.
The == algorithm, in order
- Same type? Use
===If both operands are already the same type,==and===agree completely. This is most real comparisons. nullandundefinedare equal to each other and nothing else This is a special case written directly into the spec, not a conversion. It is also the one genuinely useful thing==does.- Number against string? Convert the string with
ToNumberThe string side moves to the number world, using the same conversion as unary+. - Boolean on either side? Convert it with
ToNumberand start overtruebecomes1andfalsebecomes0. This is why comparing anything to a boolean with==is a trap. - Object against primitive?
ToPrimitivethe object and start over This is where arrays start comparing equal to numbers and strings, using the coercion rules from lesson 3.2.
a | b | a == b | a === b |
|---|---|---|---|
0 | '0' | true | false |
0 | '' | true | false |
0 | false | true | false |
'' | false | true | false |
'1' | 1 | true | false |
null | undefined | true | false |
null | 0 | false | false |
null | false | false | false |
NaN | NaN | false | false |
[] | false | true | false |
[0] | false | true | false |
{} | {} | false | false |
The rule
function greet(name) {
if (name == null) return 'hello, stranger'; // catches null AND undefined
return `hello, ${name}`;
}
console.log(greet()); // 'hello, stranger'
console.log(greet(null)); // 'hello, stranger'
console.log(greet('')); // 'hello, ' an empty name is still a name
console.log(greet('Ada')); // 'hello, Ada'
NaN and -0: where === itself is not enough
console.log(NaN === NaN); // false NaN is not equal to itself
console.log(Number.isNaN(NaN)); // true the honest test
console.log(Object.is(NaN, NaN)); // true
console.log([NaN].includes(NaN)); // true includes uses SameValueZero
console.log([NaN].indexOf(NaN)); // -1 indexOf uses ===
console.log(0 === -0); // true === cannot tell them apart
console.log(Object.is(0, -0)); // false Object.is can
console.log(1 / 0, 1 / -0); // Infinity -Infinity the difference is real
console.log(Math.sign(-0)); // -0
| Comparison | NaN vs NaN | 0 vs -0 | Use it for |
|---|---|---|---|
== | false | true | nothing, except x == null |
=== | false | true | everything, by default |
Object.is | true | false | exact identity, cache keys, change detection |
SameValueZero (includes, Map keys) | true | true | membership tests |
Objects compare by identity, never by content
const a = { id: 1 };
const b = { id: 1 };
const c = a;
console.log(a === b); // false two separate objects
console.log(a === c); // true the same object, two names
console.log([1, 2] === [1, 2]); // false
console.log(JSON.stringify([1, 2]) === '[1,2]'); // true, but fragile
function deepEqual(a, b) {
if (Object.is(a, b)) return true;
if (typeof a !== 'object' || typeof b !== 'object' || !a || !b) return false;
if (Array.isArray(a) !== Array.isArray(b)) return false;
const keys = Object.keys(a);
if (keys.length !== Object.keys(b).length) return false;
return keys.every((k) => deepEqual(a[k], b[k]));
}
console.log(deepEqual({ x: [1, 2] }, { x: [1, 2] })); // true
console.log(deepEqual({ x: 1 }, { x: 1, y: 2 })); // falseContent equality is something you write, not something you find.
That version handles the common cases in ten lines. The exercise below asks you to close the gaps it leaves: a missing key whose value is undefined, and objects with the same key count but different keys.
console.log([] == false, [] === false, [0] == false, '0' == false);[] == false: the boolean becomes 0, the array becomes '', '' becomes 0, so true. [] === false compares an object to a boolean, so false immediately. [0] == false: [0] becomes '0', which becomes 0, so true. '0' == false: false becomes 0 and '0' becomes 0, so true.
Try it yourself
Equality grid
const pairs = [
[0, '0'], [0, ''], [0, false], ['', false],
[null, undefined], [null, 0], [NaN, NaN], [[], false], [0, -0],
];
for (const [a, b] of pairs) {
const label = `${JSON.stringify(a)} vs ${JSON.stringify(b)}`;
/* eslint-disable-next-line eqeqeq */
console.log(label.padEnd(22), '==', a == b, '| ===', a === b, '| Object.is', Object.is(a, b));
}
Add rows. Try [1] == 1, ' ' == 0, new String('a') == 'a', and anything you think you can predict.
Exercises
Rebuild Object.is
Write sameValue(a, b) that behaves exactly like Object.is without calling it. Two NaNs are the same value. 0 and -0 are not. Everything else follows ===.
Write deepEqual properly
Write deepEqual(a, b) comparing structure and content. Primitives use Object.is semantics, so two NaNs match and 0 does not match -0. Objects match when they have the same own keys and every value matches recursively. An array never matches a plain object. Do not use JSON.stringify.
Check yourself
- What does this log?
- false true —
==has a hard-coded rule thatnullis only loosely equal toundefined, sonull == 0is false. Relational operators have no such rule: they runToNumber(null), which is0, sonull >= 0is true. Two operators, two different rulebooks. - Which comparison reports that
NaNequalsNaN? Object.is(NaN, NaN)—==and===both say false, becauseNaNis specified as not equal to any value including itself.Object.isuses SameValue, which treats theNaNs as the same value.Number.isNaN(x)is the usual way to test a single value.- What is the one
==comparison worth keeping in production code? x == nullto catchnullandundefined—x == nullis true for exactlynullandundefined, and false for0,'',falseandNaN. It is precise and short. The other three all depend on coercion steps that make the code harder to read than the explicit version.
Common mistakes
- Using
==with a boolean, where'1' == trueis true but'a' == trueis false. - Comparing objects or arrays with
===and expecting content comparison. - Reaching for
JSON.stringify(a) === JSON.stringify(b), which depends on key order and dropsundefined.
Takeaways
===compares type and value with no conversion. Default to it.==converts first, and the result is not transitive, so it cannot be reasoned about locally.x == nullis the one allowance: exactlynullorundefined.Object.isdiffers from===on precisely two inputs:NaNand-0.- Objects compare by identity, so content equality is code you write.