Operators, Coercion, and Equality
This chapter assumes you already know JavaScript's primitive types and how variables work. The focus is on how the language behaves at the edges: what the + operator really does, when == does something you didn't intend, and how the abstract operations behind coercion actually resolve. The goal is that you can predict results without running the code.
The `+` Operator's Three Cases
+ is the only arithmetic operator with an overload for strings. The runtime resolves it in this order:
- If either operand is an object, convert it to a primitive via
ToPrimitive(i.e.valueOf, thentoString). - If either primitive is a string, concatenate.
- Otherwise, convert both to numbers and add.
1 + '2'; // '12' — string branch wins
1 + 2; // 3
[] + {}; // '[object Object]' — both become primitives first
{} + {}; // NaN in the REPL: leading { parsed as a block, +({}) follows
'5' - 3; // 2 — subtraction has no string overload, so ToNumber applies
[6] - 1; // 5 — ToPrimitive gives '6', ToNumber gives 6Note the asymmetry: + prefers strings, every other arithmetic operator prefers numbers. That asymmetry is where most coercion surprises originate.
Loosely Equal: The `==` Algorithm
== (abstract equality) never compares an object against another object by value, and its coercion rules depend on type pairing. The comparison of '0' == false producing true is not string-to-boolean conversion; it is both sides being converted to numbers.
'0' == false; // true — ToNumber('0') is 0, ToNumber(false) is 0
'' == 0; // true — same path
null == undefined; // true — special case, nothing else equals null
null == 0; // false — null only loosely equals undefined
NaN == NaN; // false — NaN is never equal to itselfFor objects, == falls back to reference identity after attempting ToPrimitive on whichever side is an object. That makes [] == '' true ([] primitives to '') but [] == [] false.
When `==` Is Defensible
Most style guides mandate ===. The narrow case where == carries intent: testing against null/undefined in one check.
if (value == null) { /* null or undefined */ }Otherwise use ===. NaN is not equal to anything, including itself, which is why Number.isNaN(x) exists and x === x (a NaN check some libraries use) is a valid idiom.
SameValue and Its Variants
The strict comparison family has three members with different treatment of edge values:
| Operation | NaN | `+0` vs `-0` | Objects compared by |
|---|---|---|---|
| `===` | false | true | reference |
| `Object.is` | true | false | reference |
| `Object.assign` semantics | n/a | n/a | copies enumerable own props |
Object.is is what Array.prototype.includes uses internally, which is why [NaN].includes(NaN) is true but [NaN].indexOf(NaN) is -1 — indexOf uses ===. This is a real-world gotcha, not a spec curiosity.
[-0, +0].includes(-0); // true
Object.is(-0, +0); // false
Object.is(NaN, NaN); // trueToPrimitive and Symbol.toPrimitive
When an object is coerced, the engine checks in order: the well-known Symbol.toPrimitive method, then valueOf, then toString (with a hint of "string", "number", or "default"). You can and sometimes should control this deliberately:
const temp = {
[Symbol.toPrimitive](hint) {
return hint === 'string' ? '20°C' : 20;
}
};
`${temp}`; // '20°C'
temp + 5; // 25
temp == 20; // trueA valueOf that returns an object (rather than a primitive) is silently ignored and the next method is tried — a common source of "why did my custom valueOf do nothing" bugs.
Comparison Operators
<, >, <=, >= also run ToPrimitive on both sides, but there is no string-preferring overload for numeric comparison: if both primitives are strings, they compare lexicographically; otherwise numerically.
'10' < '9'; // true — string comparison, '1' < '9'
'10' < 9; // false — numeric comparisonMixed-type sorts in Array.prototype.sort without a comparator exploit exactly this behavior and produce nondeterministic-looking output across engines. Always pass a comparator.
Summary
+prefers strings; every other arithmetic operator prefers numbers.==coerces via type-pairing rules;===compares identity;Object.istreats NaN and ±0 distinctly.- Objects coerce through
Symbol.toPrimitive,valueOf, thentoString, with a hint. - Prefer explicit conversions (
Number(x),String(x)) at boundaries; reserve==for the null check.
Exercise
What does [] + [] evaluate to, and which branch of the + algorithm resolves it?
Show answer
'' — the empty string. Both arrays convert to primitives via ToPrimitive, yielding '' each; since either operand is a string, the concatenation branch applies, and '' + '' is ''.
Exercise
Why does [NaN].includes(NaN) return true while [NaN].indexOf(NaN) returns -1?
Show answer
includes uses the SameValueZero comparison (NaN equals NaN), whereas indexOf uses strict ===, under which NaN is never equal to anything, including itself.
Spotted a mistake, or code that does not work?