Episode 1 — Fundamentals / 1.18 — Operators and Type System
1.18.b — Comparison Operators
In one sentence: Comparison operators test relationships between values and return booleans — but the critical difference between
==(loose, with coercion) and===(strict, no coercion) is the single most important operator distinction every JavaScript developer must master.
Navigation: ← 1.18.a — Arithmetic Operators · 1.18.c — Logical Operators →
1. Equality operators overview
| Operator | Name | Coercion? | Recommendation |
|---|---|---|---|
=== | Strict equality | No | Default choice |
!== | Strict inequality | No | Default choice |
== | Loose (abstract) equality | Yes | Avoid unless intentional |
!= | Loose inequality | Yes | Avoid unless intentional |
2. Strict equality === and !==
=== returns true only when both the type and the value are the same. No conversion happens.
console.log(5 === 5); // true
console.log(5 === "5"); // false (number vs string)
console.log(true === 1); // false (boolean vs number)
console.log(null === undefined); // false (different types)
console.log(null === null); // true
console.log(undefined === undefined); // true
!== is the negation of ===:
console.log(5 !== "5"); // true (different types)
console.log(5 !== 5); // false
Rule: Use === and !== as your default for all comparisons.
3. Loose equality == — the abstract equality algorithm
== performs type coercion before comparing. The ECMAScript spec defines a multi-step algorithm (simplified below):
Step-by-step (simplified)
- Same type? Compare directly (like
===). null == undefined? Returntrue(special rule).- Number vs String? Convert the string to a number, then compare.
- Boolean vs anything? Convert the boolean to a number (
true → 1,false → 0), then restart. - Object vs primitive? Call
ToPrimitiveon the object, then restart.
// Step 3: string → number
console.log(5 == "5"); // true ("5" → 5)
console.log(0 == ""); // true ("" → 0)
console.log(0 == "0"); // true ("0" → 0)
// Step 4: boolean → number
console.log(true == 1); // true (true → 1)
console.log(false == 0); // true (false → 0)
console.log(true == "1"); // true (true→1, "1"→1)
console.log(false == ""); // true (false→0, ""→0)
// Step 2: null/undefined special case
console.log(null == undefined); // true (special rule)
console.log(null == 0); // false (null only == undefined)
console.log(null == ""); // false
console.log(null == false); // false
// Step 5: object → primitive
console.log([1] == 1); // true ([1].toString()="1", "1"→1)
console.log([""] == 0); // true ([""]. toString()="", ""→0)
The coercion table (common pairs)
| Expression | Result | Why |
|---|---|---|
0 == false | true | false → 0 |
"" == false | true | false → 0, "" → 0 |
"0" == false | true | false → 0, "0" → 0 |
"" == 0 | true | "" → 0 |
"0" == 0 | true | "0" → 0 |
"" == "0" | false | Same type (string), different values |
null == undefined | true | Special rule |
null == 0 | false | null only == undefined |
NaN == NaN | false | NaN never equals anything |
[] == false | true | [] → "" → 0, false → 0 |
[] == 0 | true | [] → "" → 0 |
Notice the inconsistency: "" == false is true, "0" == false is true, but "" == "0" is false. This is why == is dangerous.
4. Why === should be your default
- Predictable — no hidden conversions, no surprises.
- Readable — the reader knows you mean "same type AND value."
- Linters enforce it — ESLint's
eqeqeqrule flags==usage. - Performance — marginally faster (no coercion step), though negligible.
The one acceptable use of ==
// Checking for null OR undefined in one shot
if (value == null) {
// value is null or undefined (and nothing else)
}
// Equivalent to:
if (value === null || value === undefined) { ... }
Some style guides allow == null because the semantics are clear and useful. Everything else should use ===.
5. Relational operators: >, <, >=, <=
These compare values and return booleans. They do coerce types.
console.log(10 > 5); // true
console.log(10 < 5); // false
console.log(10 >= 10); // true
console.log(10 <= 9); // false
Number comparisons
console.log(3 > 2); // true
console.log(-1 > -5); // true
console.log(0 > -0); // false (0 and -0 are equal)
String comparisons (lexicographic / Unicode order)
Strings are compared character by character using Unicode code points:
console.log("apple" < "banana"); // true (a < b)
console.log("abc" < "abd"); // true (c < d at position 2)
console.log("a" < "A"); // false (a=97, A=65 — lowercase is higher)
console.log("10" < "9"); // true ("1"=49 < "9"=57 — string comparison!)
Gotcha: "10" < "9" is true because it compares as strings, not numbers. If you want numeric comparison, convert first:
console.log(Number("10") < Number("9")); // false (correct)
Mixed type comparisons
console.log("5" > 3); // true ("5" → 5)
console.log(true > 0); // true (true → 1)
console.log(null > 0); // false (null → 0, 0 > 0 is false)
console.log(null >= 0); // true (null → 0, 0 >= 0 is true)
console.log(null == 0); // false (== has special null rules!)
console.log(undefined > 0); // false (undefined → NaN)
console.log(undefined < 0); // false (NaN comparisons are always false)
console.log(undefined == 0); // false
Warning: null >= 0 is true but null > 0 is false and null == 0 is false. This inconsistency exists because >= uses numeric coercion but == has its own algorithm.
6. Edge cases every developer must know
NaN comparisons
console.log(NaN === NaN); // false
console.log(NaN == NaN); // false
console.log(NaN > 0); // false
console.log(NaN < 0); // false
console.log(NaN >= 0); // false
// NaN makes EVERY comparison false (except !=, !==)
console.log(NaN !== NaN); // true
0 vs -0
console.log(0 === -0); // true (spec says they're equal)
console.log(0 == -0); // true
console.log(Object.is(0, -0)); // false (Object.is distinguishes them)
console.log(1 / 0); // Infinity
console.log(1 / -0); // -Infinity (they ARE different!)
null vs undefined
console.log(null == undefined); // true (special rule)
console.log(null === undefined); // false (different types)
console.log(null == false); // false (null only == undefined)
console.log(undefined == false); // false
7. Object.is() for edge cases
Object.is() works like === but handles two edge cases differently:
| Comparison | === | Object.is() |
|---|---|---|
NaN vs NaN | false | true |
0 vs -0 | true | false |
| Everything else | Same | Same |
console.log(Object.is(NaN, NaN)); // true
console.log(Object.is(0, -0)); // false
console.log(Object.is(5, 5)); // true
console.log(Object.is("a", "a")); // true
console.log(Object.is(null, null)); // true
When to use Object.is: Rarely in everyday code. Useful in libraries, polyfills, or when you specifically need to distinguish 0 from -0 or detect NaN reliably.
8. Comparison operators in practice
Input validation
function validateAge(input) {
const age = Number(input);
if (Number.isNaN(age)) {
return "Please enter a valid number";
}
if (age < 0 || age > 150) {
return "Age must be between 0 and 150";
}
return `Age accepted: ${age}`;
}
Sorting with comparisons
const scores = [85, 92, 78, 95, 88];
// Sort ascending
scores.sort((a, b) => a - b); // [78, 85, 88, 92, 95]
// Sort descending
scores.sort((a, b) => b - a); // [95, 92, 88, 85, 78]
Clamping a value
function clamp(value, min, max) {
if (value < min) return min;
if (value > max) return max;
return value;
}
console.log(clamp(15, 0, 10)); // 10
console.log(clamp(-5, 0, 10)); // 0
console.log(clamp(7, 0, 10)); // 7
Key takeaways
===checks type and value with no coercion — use it as your default.==coerces types via the abstract equality algorithm — produces many surprising results.- The only widely accepted use of
==isvalue == nullto check fornullorundefined. - String comparison is lexicographic (Unicode order) —
"10" < "9"istrue. NaNis not equal to anything, including itself — useNumber.isNaN().null >= 0istruebutnull == 0isfalse— relational and equality operators use different coercion rules.Object.is()handles theNaNand-0edge cases that===does not.
Explain-It Challenge
Explain without notes:
- Why does
"" == falsereturntrue? Walk through the coercion steps. - What are the three results of
null > 0,null == 0, andnull >= 0? Explain the inconsistency. - Why is
NaN === NaNfalse, and how would you check if a variable holdsNaN? - Give one scenario where
Object.is()behaves differently from===. - Why should
===be your default comparison operator?
Navigation: ← 1.18.a — Arithmetic Operators · 1.18.c — Logical Operators →