Every JavaScript developer uses var, let, and const — but few understand what actually happens when you declare a variable. This article walks through scope, hoisting, and the Temporal Dead Zone with examples you can edit and run.
Prerequisites: None — this is the starting point for the JavaScript Foundations series.
0. What Is a Variable?
A variable is a named container in memory that holds a value. When you write let age = 25, three things happen:
- Declaration — you tell the engine "I want a variable named
age" - Initialization — the engine sets aside memory for it (and may fill it with
undefined) - Assignment — the value
25is placed into that memory
Different keywords (var, let, const) change when initialization happens and where the variable lives — that's what scope and hoisting are about.
Think of it like this:
let age = 25;
// ┬ ┬
// │ └── Assignment (put 25 in the box)
// └────── Declaration + Initialization (create the box)
1. The Three Ways to Declare Variables
JavaScript gives you three keywords: var (function-scoped, hoisted with undefined), let (block-scoped, TDZ), and const (block-scoped, TDZ, cannot be reassigned).
Loading editor...
If you uncomment the z = 31 line and run it, you'll see the runtime error. const prevents reassignment but does not make objects immutable:
Loading editor...
2. Function Scope vs Block Scope
var is scoped to the nearest function. let and const are scoped to the nearest block ({ }).
Loading editor...
This is the single biggest reason var is considered harmful: it doesn't respect block boundaries, so a variable declared inside an if or for leaks into the surrounding function. This leads to accidental overwrites, closure bugs in loops, and code that's hard to reason about. let and const fix all three by being block-scoped and creating fresh bindings per loop iteration.
setTimeout schedules a function to run later, after the loop has already finished. With var, all three callbacks point to the same i (now 3). With let, each iteration gets its own j with the value frozen at that iteration.
Loading editor...
3. Global Scope — The window Difference
var in global scope attaches itself to the global object (window in browsers, globalThis everywhere). let and const do not — they exist in a separate global lexical environment.
Loading editor...
This matters because window properties can collide with built-ins (name, status, top) and third-party scripts. let/const keep your variables isolated.
4. Redeclaration Rules
var lets you redeclare the same variable name in the same scope — no error, just silently overwrites. let and const throw a SyntaxError if you try.
Loading editor...
The rule: you can redeclare var anywhere in the same function. You cannot redeclare a let/const in the same block scope. However, you CAN use the same let name in a nested block — that's shadowing (covered below), not redeclaration.
5. Hoisting — What Gets Lifted
During compilation, the engine registers all declarations before executing code. var declarations are hoisted and initialized to undefined. let and const are hoisted but not initialized — they exist in the TDZ.
Loading editor...
Here's the mental model of what the engine does:
// What you write:
console.log(x);
var x = 5;
// What the engine sees after hoisting:
var x = undefined; // Declaration hoisted, initialized to undefined
console.log(x); // undefined
x = 5; // Assignment stays in place
For let/const, the declaration is hoisted but the variable remains uninitialized until the let/const line executes:
Loading editor...
Even typeof — the operator that's safe for undeclared variables — throws inside the TDZ:
Loading editor...
6. Function Declarations vs Function Expressions
Function declarations are hoisted completely — name AND body. Function expressions (assigning to a variable) follow the variable's hoisting rules.
Loading editor...
Arrow functions assigned to const are also not hoisted:
Loading editor...
7. Shadowing — When Inner Blocks Hide Outer Variables
A variable in an inner scope shadows a variable with the same name in an outer scope if it's declared with the same keyword or a more restrictive one.
Loading editor...
Illegal shadowing — var cannot shadow let because var is scoped to the function, not the block. So when the engine hoists var, it tries to register it in the same scope as the outer let — causing a collision:
Loading editor...
8. The Scope Chain — How the Engine Looks Up Variables
When you reference a variable, the engine walks the scope chain from inner to outer until it finds a match — or throws ReferenceError.
Global Scope
├── outerVar (not here — go deeper)
├── global ✅ FOUND
│
└── outer() Scope
├── innerVar (not here — go deeper)
├── outerVar ✅ FOUND
│
└── inner() Scope
├── innerVar ✅ FOUND
└── (not here → check outer)
Loading editor...
This scope chain is what makes closures possible — inner functions retain access to their outer scope variables even after the outer function returns.
9. var Hoisting in Functions
var inside a function is hoisted to the top of that function, not to the global scope:
Loading editor...
Key Takeaways
| Keyword | Scope | Hoisted? | Initialized? | Can Reassign? |
|---|---|---|---|---|
var | Function | Yes | To undefined | Yes |
let | Block | Yes (TDZ) | No — TDZ until assignment | Yes |
const | Block | Yes (TDZ) | No — TDZ until assignment | No |
function | Block/Function | Yes (fully) | Name + body | N/A |
- Hoisting is not physical movement — it's the compile-time registration of declarations.
- TDZ means the variable exists but accessing it before the
let/constline throwsReferenceError. - Always use
constby default,letwhen reassignment is needed, and nevervarunless maintaining legacy code. - Function declarations are fully hoisted; function expressions follow variable rules.
Next: JS Foundations #2 — this Demystified — the four binding rules that determine what this points to in every situation.
