Values and Types; Variables, let, const, and Scope
Prerequisites
This chapter assumes you have written JavaScript before, know what a function call is, and have at least run scripts in Node or a browser console. We move quickly and focus on the type system's internal rules and scoping edge cases rather than syntax basics.
The Type System
JavaScript has exactly eight types. Seven are primitives: undefined, null, boolean, number, bigint, string, and symbol. The eighth is object. Everything else—arrays, functions, dates—is an object. Primitives are immutable and compared by value; objects are compared by identity.
The critical distinction is static shape versus runtime value. typeof null === "object" is a historical bug the language cannot fix. typeof returns "function" for callables, and typeof undefined is the reliable way to test for an unset value. For arrays, use Array.isArray(x), because typeof [] === "object".
Numbers are IEEE-754 doubles. All integers above Number.MAX_SAFE_INTEGER (2^53 − 1) lose precision—JSON parsing of large IDs is the classic failure. bigint exists for this but does not mix with number in arithmetic without explicit conversion.
Coercion Is a Protocol, Not Chaos
Coercion follows spec-defined algorithms. The rules worth memorizing:
| Expression | Result | Rule applied |
|---|---|---|
| `1 + "2"` | `"12"` | Addition prefers string if either operand is a string |
| `"5" - 2` | `3` | Subtraction forces numeric |
| `[] + {}` | `"[object Object]"` | `ToPrimitive`: array `toString`, object `valueOf` then `toString` |
| `null == undefined` | `true` | Only pair `==` treats specially |
| `null >= 0` | `true` | Relational operators convert `null` to `0` |
The asymmetry that trips people up: null == 0 is false (special case) while null >= 0 is true (numeric conversion). Prefer === everywhere; know =='s rules anyway, because you will read code that uses it.
console.log(9007199254740993n + 1n); // 9007199254740994n — bigint stays exact
console.log(0.1 + 0.2 === 0.3); // false — binary floating point
console.log([1,2] == "1,2"); // true — ToPrimitive then string compareVariables, let, const, and Scope
let and const are block-scoped and live in the Temporal Dead Zone (TDZ): the binding exists from the top of the block, but reading it before the declaration line throws a ReferenceError. This is why hoisting for let/const is uninitialized hoisting, distinct from var's hoisting-to-undefined.
{
// console.log(x); // ReferenceError: Cannot access 'x' before initialization
const x = 1;
}const freezes the binding, not the value: const a = []; a.push(1) is legal. For deep immutability you need Object.freeze (shallow) plus recursion.
var is function-scoped, initialized to undefined at function entry, and attaches to globalThis at top level in scripts. In modules and classes it never appears; in legacy code it causes the loop-capture bug that let fixes:
for (var i = 0; i < 3; i++) setTimeout(() => console.log(i)); // 3, 3, 3
for (let j = 0; j < 3; j++) setTimeout(() => console.log(j)); // 0, 1, 2Each let iteration creates a fresh binding; var has one binding shared by all closures.
Show diagram source
graph TD
subgraph var loop
F[function scope: var i] --> C1[callback] & C2[callback] & C3[callback]
end
subgraph let loop
B1[block: let j=0] --> K1[callback]
B2[block: let j=1] --> K2[callback]
B3[block: let j=2] --> K3[callback]
endExercise
Why does null == 0 evaluate to false while null >= 0 evaluates to true?
Show answer
The == algorithm has a special case: null is only loosely equal to undefined and itself. Relational operators have no such case; they convert null to the number 0, so 0 >= 0 is true.
Exercise
What does typeof return for x declared as let x; inside a block, if you call typeof x before the declaration?
Show answer
It does not return "undefined". The binding is in the Temporal Dead Zone, so the expression throws a ReferenceError.
Spotted a mistake, or code that does not work?