JAVASCRIPT / OPERATORS
Comparison operators and ordering rules
Predict the result of any JavaScript comparison by knowing when an operator converts its operands and when it compares types directly.
What you will learn
- Choose between === and == by naming the conversion == performs before comparing
- Order strings knowing < uses UTF-16 code units, so "Z" < "a" and "10" < "9"
- Explain why null >= 0 is true while null == 0 is false
- Use Object.is or Number.isNaN where === answers wrongly for NaN and -0
Understanding Comparison operators and ordering rules
JavaScript has two comparison families and they follow different rules. The equality operators === and !== check the type first: if the types differ the answer is false immediately, with no conversion attempted. == and != run a short fixed table of conversions before comparing, which is why 0 == '0' is true (the string goes through Number()) while 0 === '0' is false. The mental model: === asks whether this is the same value of the same type, == asks whether these two could be made into the same value.
The relational operators <, >, <=, >= never perform a type check at all. Each operand is first converted to a primitive, and then exactly one of two comparisons runs: if both primitives are strings they are compared position by position using UTF-16 code unit numbers, and otherwise both are converted to numbers. That single branch explains '2' > '10' being true, because code unit '2' (50) beats '1' (49) at the first position, while '2' > 10 is false, because a mixed pair becomes 2 > 10. The same rule puts all capitals before all lowercase letters, since 'Z' is 90 and 'a' is 97.
The special values are where ordering surprises people. NaN is unordered, so every relational comparison touching it is false and NaN === NaN is false too, which means x <= y and x >= y can both be false for the same pair. null is loosely equal only to undefined, never to 0 or '', yet the relational operators convert it with Number(null), which is 0, so null >= 0 is true while null == 0 is false. Objects compare by reference under both === and ==, so two arrays with identical contents are never equal to each other, although == will compare an object to a primitive by converting the object first.
const show = (label, value) => console.log(label + ' -> ' + value);
show('0 == "0"', 0 == '0');
show('0 === "0"', 0 === '0');
show('NaN === NaN', NaN === NaN);
show('null == undefined', null == undefined);
show('null == 0', null == 0);
show('null >= 0', null >= 0);
show('"2" > "10"', '2' > '10');
show('"2" > 10', '2' > 10);Equality operators decide whether to convert at all (== does, === does not), while relational operators always convert first and then compare either two strings by code unit or two numbers.
Worked examples
Default sort uses string order
Shows that sorting without a comparator applies the same code-unit ordering that < and > use on strings.
const nums = [10, 9, 1, 2];
console.log([...nums].sort().join(','));
console.log([...nums].sort((a, b) => a - b).join(','));
const names = ['Zoe', 'apple', 'Ana'];
console.log([...names].sort().join(','));
console.log([...names].sort((a, b) => a.localeCompare(b, 'en')).join(','));Example explained
Line 1sort() with no comparator converts every element to a string first, so 10 lands between 1 and 2.
Line 2(a, b) => a - b returns a negative, zero, or positive number, which is the numeric ordering contract sort expects.
Line 3String ordering is code-unit ordering, so 'Zoe' comes before 'apple' because 'Z' is 90 and 'a' is 97.
Line 4localeCompare with an explicit locale compares letters case-insensitively at the primary level, giving the order a reader expects.
null and undefined against 0
Contrasts what loose equality does with null, undefined and empty strings against what the relational operators do.
const cases = [
['null', null],
['undefined', undefined],
['0', 0],
['""', ''],
['"0"', '0'],
];
for (const [label, v] of cases) {
console.log(label + ': == 0 -> ' + (v == 0) + ', >= 0 -> ' + (v >= 0));
}Example explained
Line 1null == 0 is false because loose equality never converts null to a number; null matches only null and undefined.
Line 2null >= 0 is true because >= sends both sides through numeric conversion and Number(null) is 0.
Line 3undefined >= 0 is false because Number(undefined) is NaN and every relational comparison with NaN is false.
Line 4'' == 0 is true because the string side is converted with Number(''), which is 0, not because '' is falsy.
Identity, Object.is, and object coercion
Demonstrates that objects compare by reference and where Object.is deliberately differs from ===.
const a = { id: 1 };
const b = { id: 1 };
const alias = a;
console.log(a === b, a === alias);
console.log(Object.is(NaN, NaN), NaN === NaN);
console.log(Object.is(0, -0), 0 === -0);
console.log([1, 2] == '1,2');Example explained
Line 1a === b is false because object equality compares references, not the properties inside.
Line 2alias holds the same reference, so a === alias is true; the language has no deep-equality operator.
Line 3Object.is differs from === in exactly two places: NaN equals NaN, and +0 is distinct from -0.
Line 4[1, 2] == '1,2' is true because == converts the array to its primitive string form and then compares two strings.
Important notes
<= is not shorthand for '< or =='. The specification defines x <= y as 'y is not less than x', and any comparison involving NaN produces an unordered result that collapses to false, which is why NaN <= NaN is false.
Two Date objects order correctly with < and > because they convert to timestamps, but d1 === d2 and d1 == d2 compare references; use d1.getTime() === d2.getTime() to test for the same instant.
Common mistakes
Writing a range check as if (0 < x < 100): it parses as (0 < x) < 100, the boolean becomes 0 or 1, and both are below 100, so the check accepts every value including 5000. Write 0 < x && x < 100.
Comparing values straight out of an input field: '9' > '18' is true because both sides are strings, so an age or price limit silently lets the wrong values through.
Testing for NaN with x === NaN or x == NaN: NaN is not equal to itself, so the branch never runs and the bad value flows onward. Use Number.isNaN(x).
Try it yourself
Change, predict, then run
In a browser console, predict then evaluate ['9', '10', '100', '8'].sort() and the same array sorted with (a, b) => a - b. Then compare '9' > '10' with Number('9') > Number('10') and state in one sentence which conversion rule produced each answer.
Open the JavaScript workspaceCheck your understanding
Why does null >= 0 evaluate to true when both null == 0 and null > 0 are false?
- >= converts both operands to numbers and Number(null) is 0, while loose equality has a fixed rule that pairs null only with undefined
- >= is evaluated as null > 0 || null == 0, and the second half is true because null is falsy
- null is coerced to false and false equals 0 for every comparison operator
- Relational operators return true whenever an operand is null, since null cannot be ordered
Show answer
Relational operators convert their operands to primitives and then to numbers, and Number(null) is 0, so null >= 0 becomes 0 >= 0. Option 2 is tempting because >= reads like 'greater than or equal to', but if it really were the OR of > and == the answer would be false, since both of those are false here; the language instead defines x >= y as the negation of x < y.