Sitemap

JavaScript Scope, Visualized: A Deep Dive for every skill level

7 min readJul 25, 2026

--

Everything you need to know about var, let, const, hoisting, the Temporal Dead Zone, closures, and the scope chain — with diagrams.

Press enter or click to view image in full size

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.
Press enter or click to view image in full size

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 if or a for loop) → 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();
Press enter or click to view image in full size

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.

  • var declarations are hoisted and initialized with undefined immediately. So reading it early gives you undefined, not an error.
  • let and const declarations are hoisted too, but they are not initialized. From the top of the scope until the actual let/const line executes, the variable exists in a locked state called the Temporal Dead Zone (TDZ). Touching it throws.
Press enter or click to view image in full size

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, 3
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// prints: 0, 1, 2

With 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.

Press enter or click to view image in full size

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()); // 3

makeCounter() 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.

Press enter or click to view image in full size

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 = {}; // TypeError

const locks the binding (the variable-to-value link), not the contents of an object or array.

Press enter or click to view image in full size

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.

--

--

Kevan Gol
Kevan Gol

Written by Kevan Gol

Building scalable systems by day, writing about them by night. Cloud • AI • DevOps • Software Engineering.