Most JavaScript tutorials teach you syntax and let the mental model form by accident. That accidental model breaks the moment you hit a bug that depends on how the engine actually works — a mutated array passed by reference, a [] == ![] that somehow evaluates to true, a var that was hoisted into existence before its declaration line ran. This module builds the model deliberately, from memory layout up, so nothing about the language surprises you again.
1. Data Types & Memory: Primitive vs. Reference
JavaScript has exactly two categories of value. Primitives — string, number, boolean, null, undefined, symbol, bigint — are immutable and compared by value. Reference types — object, array, function, and everything built on object — are mutable and compared by identity (memory address), not content.
Why the distinction exists at the engine level
- Primitives have a fixed, known size at compile time (a 64-bit float, a boolean bit, etc.), so the engine can allocate them on the Stack — a fast, LIFO (last-in-first-out) region with O(1) push/pop and no fragmentation
- Objects have unbounded, dynamic size (an array can grow, an object can gain properties), so they must live on the Heap — a much larger, unordered memory region managed by the garbage collector
- A Stack frame for a function call holds primitive locals directly, plus, for every reference-type local, only a pointer (a memory address) into the Heap — never the object's actual bytes
- This is why assigning
let b = acopies the pointer, not the object: bothaandbnow point at the same Heap allocation
// PRIMITIVES: copied by value. Each variable owns an independent slot on the Stack.
let a = 10;
let b = a; // b gets its OWN copy of the value 10
b = 20;
console.log(a); // 10 -- a was never touched
// REFERENCE TYPES: the variable holds a pointer into the Heap, not the object itself.
const objA = { count: 10 };
const objB = objA; // objB copies the POINTER, not the object
objB.count = 20;
console.log(objA.count); // 20 -- both variables point at the same Heap allocation
// Reassigning objB to a NEW object breaks the link -- it just points objB
// at a different Heap address. objA's pointer is untouched.
objB = { count: 99 };
console.log(objA.count); // still 20| Aspect | Stack (Primitives) | Heap (Reference Types) |
|---|---|---|
| Allocation speed | O(1) — just moves a stack pointer | Slower — requires a free-space search |
| Size | Fixed, known at compile time | Dynamic, can grow or shrink |
| Lifetime management | Automatic — popped when the frame returns | Garbage collected — see Module 4 |
Comparison (===) | By value | By reference (identity) |
| Copy semantics | Full copy on assignment | Pointer copy on assignment |
2. Type Coercion Unmasked
Coercion is JavaScript converting a value from one type to another. Explicit coercion is a conversion you write yourself (String(123), Number('42')). Implicit coercion happens automatically when an operator or context demands a specific type — and it is governed by precise abstract operations defined in the ECMAScript spec, not by folklore about "JavaScript being weird."
The three abstract operations that drive every coercion
- ToPrimitive(input, hint): if input is already a primitive, return it. Otherwise, call methods on the object to extract a primitive, guided by a hint of 'string', 'number', or 'default'
- ToPrimitive's default algorithm (hint 'default' or 'number') tries input[Symbol.toPrimitive] first if defined, then input.valueOf(), then input.toString() -- the first one that returns a primitive wins
- ToPrimitive with hint 'string' (used by template literals and String()) flips the order: toString() is tried before valueOf()
- ToNumber(primitive): converts the primitive to a number using type-specific rules -- '' becomes 0, ' 42 ' becomes 42 (whitespace trimmed), true becomes 1, null becomes 0, undefined becomes NaN
- ToString(primitive): converts the primitive to a string -- true becomes 'true', null becomes 'null', an array joins its elements with commas via Array.prototype.toString
// The + operator calls ToPrimitive with hint 'default' on both operands.
// Objects fall back to valueOf() then toString().
const obj = {
valueOf() { return 42; },
toString() { return 'fallback-string'; }
};
console.log(obj + 1); // 43 -- valueOf() won, returned a primitive number
// Template literals force hint 'string' -- toString() is tried FIRST.
const objStringHint = {
valueOf() { return 42; },
toString() { return 'I am a string'; }
};
console.log(`${objStringHint}`); // "I am a string"
// Date is the one built-in whose Symbol.toPrimitive prefers 'string' even
// under the 'default' hint -- this is a deliberate spec carve-out.
console.log(new Date(0) + 1); // string concatenation, NOT a numeric add3. The Execution Context
An Execution Context is the environment the engine constructs to evaluate a chunk of code. There are three kinds -- Global (created once, when the script first loads), Function (created fresh on every function call), and Eval (rare, created for eval()). Every context is built in two distinct phases: the Creation Phase, which runs before a single line of your code executes, and the Execution Phase, which runs your code top to bottom.
What actually happens during the Creation Phase
- The engine scans the entire scope for
vardeclarations and function declarations FIRST, before executing anything -- this pre-scan is what people call 'hoisting' - Every
varis registered in the Variable Environment and initialized toundefinedimmediately -- this is why reading a var before its declaration line givesundefinedinstead of a ReferenceError - Every function declaration (not expression) is registered with its ENTIRE function body already attached -- this is why you can call a function declaration before the line it's written on
letandconstare also registered during this phase, but WITHOUT an initial value -- they sit in the 'Temporal Dead Zone' (TDZ) and throw a ReferenceError if accessed before their actual declaration line runs- The
thisbinding and the outer Lexical Environment reference (the scope chain link) are also determined during this phase, before your code runs
console.log(typeof varDeclared); // 'undefined' -- registered and initialized during Creation Phase
console.log(typeof funcDeclared); // 'function' -- entire function is hoisted with its body
try {
console.log(letDeclared); // throws
} catch (e) {
console.log(e instanceof ReferenceError); // true -- letDeclared is in the Temporal Dead Zone
}
var varDeclared = 'assigned';
function funcDeclared() { return 'I was hoisted whole'; }
let letDeclared = 'assigned';
// Function EXPRESSIONS do not get this treatment -- only the var binding hoists,
// not the assignment.
console.log(typeof funcExpression); // 'undefined', not 'function'
var funcExpression = function () { return 'not hoisted with my body'; };4. Scope & Closures
Lexical scoping means a variable's accessibility is determined by WHERE it is physically written in the source code, not by how the code is called. When the engine can't resolve an identifier in the current Lexical Environment, it walks up the reference to the outer environment recorded at CREATION time -- forming the Scope Chain. A closure is simply a function that retains a live reference to its outer Lexical Environment, even after the outer function has returned and its execution context has been popped off the Stack.
The mechanical definition of a closure
- Every function, at creation, stores an internal [[Environment]] reference pointing to the Lexical Environment it was DEFINED in -- not the one it's later CALLED from
- Normally, when a function returns, its execution context (and the Variable Environment inside it) becomes eligible for garbage collection because nothing references it anymore
- If an inner function that references an outer variable is returned or stored somewhere that outlives the outer call, its [[Environment]] pointer keeps that outer Variable Environment alive -- the GC cannot reclaim it
- This is closure: NOT 'the function remembers its variables' in some abstract sense, but a literal, mechanical consequence of a live pointer keeping a Heap-allocated environment record from being collected
// PRACTICAL APPLICATION: a private, encapsulated counter using closures
// instead of exposing mutable state on an object.
function createCounter(initialValue = 0) {
let count = initialValue; // lives in createCounter's Lexical Environment
// Both closures below capture the SAME `count` binding, not separate copies.
return {
increment() { return ++count; },
decrement() { return --count; },
getValue() { return count; },
};
}
const counterA = createCounter(10);
const counterB = createCounter(100); // independent closure -- own `count`
counterA.increment();
counterA.increment();
console.log(counterA.getValue()); // 12
console.log(counterB.getValue()); // 100 -- untouched, separate Lexical Environment
// `count` is NOT reachable from outside -- there is no `counterA.count`.
console.log(counterA.count); // undefined -- true encapsulation, not convention-based (_count)// BAD WAY (Don't Do This): global mutable counter -- no encapsulation,
// any code anywhere can read or corrupt the state, and it never gets GC'd.
let globalCount = 0;
function incrementGlobal() { globalCount++; }
// ARCHITECT WAY (Do This): closure-scoped state, exposed only through
// a controlled interface. The `count` binding is unreachable except via
// the returned methods -- true information hiding without `class` or `#`.
function createSecureCounter() {
let count = 0;
return Object.freeze({
increment: () => ++count,
getValue: () => count,
});
}Module 1 Recap
You now have the bedrock: why the Stack/Heap split exists and what it means for copy semantics, the exact abstract operations behind every implicit coercion (and why [] == ![] is true by mechanical necessity, not magic), the two-phase construction of an execution context that produces hoisting and the Temporal Dead Zone, and the precise definition of a closure as a live pointer keeping a Lexical Environment record alive on the Heap. Module 2 builds directly on this foundation to dissect the prototype chain, property descriptors, and the four rules governing this.