JavaScript Scope, Visualized: A Deep Dive for every skill level
Everything you need to know about var, let, const, hoisting, the Temporal Dead Zone, closures, and the scope chain — with diagrams.
Why Scope Matters
Every JavaScript bug you’ve ever chased at 2 AM — “why is this variable undefined?", "why did my loop print 5 five times?", "why does this function still remember an old value?" — traces back to one concept: scope.
Scope is simply the answer to one question: “From where in my code can I see this variable?”
Let’s build the idea from the ground up.
Part 1: The 5-Year-Old Explanation 🧸
Imagine your house has rooms.
- The living room is shared — everyone in the house can walk in and use whatever’s there.
- Your bedroom is private — only you can use what’s inside it. Your sister can’t grab your toys from her room.
- If you put a toy in a box inside your bedroom, only you can open that box. Not even someone standing in the hallway can reach in.
Variables work the same way:
- Declare a variable outside everything → the whole house (whole file) can see it.
- Declare it inside a room (a function) → only that room can see it.
- Declare it inside a box (a block, like an
ifor aforloop) → only that box can see it.
That’s scope. Everything else in this article is just details on how strict the walls are.
Part 2: The Beginner Explanation — Three Kinds of Walls
JavaScript has three ways to declare a variable, and each builds different walls: var, let, and const.
var — the leaky wall
var doesn't respect the toy box (block). It only respects the room (function) or the house (global).
function example() {
if (true) {
var toy = "car";
}
console.log(toy); // "car" — leaked out of the {} block!
}let and const — solid walls
let and const respect every set of { } — function, if, for, even a bare block.
function example() {
if (true) {
let toy = "car";
}
console.log(toy); // ReferenceError: toy is not defined
}Rule of thumb for junior developers: default to const. Use let only when you truly need to reassign. Avoid var entirely in modern code — it exists today mostly for backward compatibility with code written before 2015.
Part 3: The Scope Chain — How JavaScript Looks Things Up
When JavaScript sees a variable name, it doesn’t just check one room. It walks outward, room by room, until it finds the variable or runs out of rooms.
const country = "Global";
function outer() {
const country2 = "Outer";
function inner() {
const country3 = "Inner";
console.log(country3); // found in Inner ✅
console.log(country2); // not in Inner, walk out → found in Outer ✅
console.log(country); // not in Inner, not in Outer → found in Global ✅
}
inner();
}
outer();This outward-walking search is called the scope chain. It’s determined entirely by where the function was written in the code — not where it’s called from. That’s why this behavior is called lexical scoping (or static scoping): the walls are drawn at write-time, based on the source code’s physical structure, and never move afterward.
Part 4: Hoisting and the Temporal Dead Zone (Where Juniors Get Tripped Up)
Here’s something that confuses almost every developer the first time they see it:
console.log(x); // undefined (not an error!)
var x = 5;console.log(y); // ReferenceError: Cannot access 'y' before initialization
let y = 5;Why the difference?
Get Kevan Gol’s stories in your inbox
Join Medium for free to get updates from this writer.
Hoisting means JavaScript actually scans a scope for declarations before it runs any code line-by-line. Think of it as JavaScript skimming the room for furniture labels before you’re allowed to walk in.
vardeclarations are hoisted and initialized withundefinedimmediately. So reading it early gives youundefined, not an error.letandconstdeclarations are hoisted too, but they are not initialized. From the top of the scope until the actuallet/constline executes, the variable exists in a locked state called the Temporal Dead Zone (TDZ). Touching it throws.
Why does the TDZ exist? It’s a deliberate safety rail. var's silent undefined hid real bugs for years — code would "work" while quietly using a value that wasn't ready yet. The TDZ turns that silent bug into a loud, immediate error.
Part 5: The Classic Interview Question — var vs let in Loops
This single example explains why let was added to the language.
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// prints: 3, 3, 3for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// prints: 0, 1, 2With var, there is only one i, shared across the whole loop, living in the function/global scope. By the time the callbacks run, the loop has already finished and i is 3.
With let, JavaScript creates a brand new i for each iteration — a fresh toy box every lap around the loop. Each callback closes over its own copy.
This is the single most-asked JavaScript scope interview question, and now you can draw the answer.
Part 6: Closures — Scope’s Superpower
A closure is what happens when an inner function “remembers” the variables from its outer scope, even after the outer function has finished running.
function makeCounter() {
let count = 0;
return function () {
count += 1;
return count;
};
}
const counter = makeCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3makeCounter() finished executing long ago. Normally its local variables would be thrown away. But the inner function kept a live reference to count, so the scope stays alive as long as something can still reach it.
This is how private state, memoization, event handlers, and most of React’s useState work under the hood are built. Closures aren't an advanced trick bolted onto scope — they're what scope is, once you stop thinking of a function call as something that instantly forgets everything.
Part 7: Senior-Level Nuances
A few things that separate “I know scope” from “I really know scope”:
1. Module scope isn’t global scope. In an ES module (<script type="module">, or any .mjs/bundler-processed file), top-level const/let/var/function declarations belong to that module's scope, not the global window/globalThis. Two modules can each declare const x without collision.
2. Function scope vs. block scope is a compile-time property, not a runtime one. Engines (like V8) statically analyze scope during parsing/compilation to build scope information before execution — this is part of what makes the TDZ enforceable and what makes closures efficient rather than requiring a full re-scan on every call.
3. this is not scope. This trips up even experienced developers. this is dynamic (determined by how a function is called), while variable scope is lexical (determined by where code is written). Arrow functions don't have their own this — they inherit it lexically from their enclosing scope, which is why arrow functions are so useful inside closures and callbacks.
4. Memory implications of closures. Every closure keeps its referenced outer variables alive in memory for as long as the closure itself is reachable. In long-lived closures (event listeners, caches, timers you forget to clear), this can quietly become a memory leak. Senior engineers audit what’s captured, not just what’s returned.
5. const doesn't mean immutable — it means "can't be reassigned."
const user = { name: "Ada" };
user.name = "Grace"; // totally legal
user = {}; // TypeErrorconst locks the binding (the variable-to-value link), not the contents of an object or array.
Closing Thought
Scope is the map of who can see what, and for how long. var, let, and const just change how thick the walls are. Hoisting and the TDZ describe when a name becomes usable. Closures are what happens when a wall stays standing even after the room around it has been demolished.
Once you can draw the boxes, JavaScript stops feeling like magic — it starts feeling like architecture.








