Experts debug 'impossible' results by knowing exactly which algorithm the spec runs.
The ECMAScript spec defines behaviour through abstract operations. A handful explain most of JavaScript's notorious edge cases.
ToPrimitive(value, hint) converts objects to primitives. It first calls value[Symbol.toPrimitive](hint) if present. Otherwise, with hint "number" or "default" it tries valueOf() then toString(); with hint "string" the reverse. Plain objects' valueOf returns the object itself, so toString wins — giving "[object Object]" and, for arrays, the elements joined by commas. Date treats "default" as "string".
The `+` operator applies ToPrimitive with hint "default" to both sides; if either result is a string, it concatenates, otherwise it does numeric addition. So [1, 2] + [3] is "1,23" and [] + {} is "[object Object]". (Typed at the start of a console line, {} + [] is 0: the {} parses as an empty block, leaving unary +[].)
IsLooselyEqual (`==`): null and undefined equal each other and nothing else; booleans are converted to numbers first; objects compared with primitives go through ToPrimitive; strings and numbers compare numerically. That's why [] == false is true ([] → "" → 0, false → 0).
Four equalities: ==, === (where NaN !== NaN and 0 === -0), SameValue (Object.is: NaN equals itself, 0 and -0 differ) and SameValueZero (like SameValue but 0 equals -0), used by includes, Map and Set. indexOf uses ===, so [NaN].indexOf(NaN) is -1 while [NaN].includes(NaN) is true.
Property order (OrdinaryOwnPropertyKeys): integer-like keys in ascending numeric order, then string keys in insertion order, then symbols in insertion order. Object.keys, JSON.stringify and for...in (for own properties) follow it.
Example
const weird = {
valueOf() { console.log("valueOf"); return 42; },
toString() { console.log("toString"); return "str"; },
};
console.log(weird + 1); // valueOf, then 43 (hint "default")
console.log(`${weird}`); // toString, then "str" (hint "string")
console.log([1, 2] + [3]); // "1,23"
console.log([] == false); // true: [] -> "" -> 0, false -> 0
console.log(null == 0, null >= 0); // false true (== vs relational rules)
console.log(Object.is(-0, 0), -0 === 0, 1 / -0); // false true -Infinity
console.log([NaN].indexOf(NaN), [NaN].includes(NaN)); // -1 true
console.log(new Set([0, -0]).size); // 1 (SameValueZero)
const o = { b: 1, 2: "two", a: 2, 1: "one", [Symbol("s")]: 3 };
console.log(Reflect.ownKeys(o)); // [ '1', '2', 'b', 'a', Symbol(s) ]Remember
- ToPrimitive:
Symbol.toPrimitive, else valueOf/toString in hint-dependent order +concatenates if either primitive is a string;==coerces via ToPrimitive and ToNumberincludes/Map/Set use SameValueZero;indexOfuses===;Object.isis SameValue
Watch out for
- Using
indexOfto look forNaN— it always returns -1.