Loading...
Loading...
Why some code works before it appears. Declarations move to the top.
Module 6 · How JavaScript Works
Understanding the engine makes you a better debugger. Execution contexts, the call stack, hoisting, memory allocation, closures, and the prototype chain explain why JavaScript behaves the way it does under the hood.
Hoisting moves declarations to the top of their scope during compilation — which is why functions can run before their line and var reads as undefined instead of throwing.
JavaScript looks simple on the surface, but the engine does a lot of work behind the scenes. Variables are hoisted, functions create new execution contexts, the call stack tracks nested calls, and the prototype chain resolves property lookups.
This module is for developers who want to stop guessing and start understanding. Visual diagrams of the call stack, heap, and scope chain make abstract concepts concrete — and directly improve your debugging skills in real projects.
Hoisting explains some of JavaScript's most surprising behavior. It appears in virtually every interview, causes subtle var bugs in loops, and makes the case for let, const, and the temporal dead zone.
JavaScript moves declarations to the top of their scope before executing.
What you write:
greet();
function greet() {
console.log("Hello!");
}What the engine sees:
function greet() {
console.log("Hello!");
}
greet(); // ✓ works!💡 "Hoisting" isn't literally moving code. The engine scans declarations first (creation phase), then executes. It just appears like declarations moved up.
Function declarations are fully hoisted — usable anywhere in their scope.
// ✓ Works — function declarations are hoisted: const result = add(2, 3); // 5 function add(a, b) { return a + b; } // ✗ Fails — function expressions are NOT hoisted: const result2 = multiply(2, 3); // TypeError! const multiply = function(a, b) { return a * b; }; // ✗ Also fails — arrow functions: const result3 = divide(10, 2); // TypeError! const divide = (a, b) => a / b;
✓ Hoisted (fully)
function name() {}
✗ NOT hoisted
const fn = function() {}
const fn = () => {}
var declarations are hoisted but NOT their assignments.
console.log(x); // undefined (not ReferenceError!) var x = 5; console.log(x); // 5
var is hoisted with a value of undefined. The assignment (= 5) stays in place and runs during execution.
let and const exist but can't be accessed until their declaration.
// The Temporal Dead Zone (TDZ): { // ┌── TDZ for 'x' starts ──┐ // │ │ console.log(x); // ReferenceError! // │ │ // └── TDZ for 'x' ends ─────┘ let x = 10; // ← x is now initialized console.log(x); // 10 ✓ } // Same for const: console.log(PI); // ReferenceError! const PI = 3.14; console.log(PI); // 3.14 ✓
Timeline of a let/const variable:
↑ declaration reached
What happens when names collide.
// Function declarations beat var declarations: var foo = "variable"; function foo() { return "function"; } console.log(typeof foo); // "function" // Function was hoisted above the var // But assignment wins in execution: var bar = "I win"; function bar() { return "function"; } console.log(bar); // "I win" // After execution, the assignment overwrites // Function declarations beat each other (last wins): function x() { return 1; } function x() { return 2; } x(); // 2 — last declaration wins
💡 Best practice: Use const/let and avoid relying on hoisting. Declare everything before use. Problems with hoisting are why let/const were created.
Common questions about hoisting.