JavaScript is not just a language, it’s a stage 🎭 where functions are the actors, scopes are the stage lights 🎥, and closures are the secret scripts they carry in their pockets.
Today, we’re sneaking past the curtain 🎟️, into JS’s backstage, and discovering how these things really work.
By the end, you’ll see why JS sometimes feels like a reality-bending trickster language and you’ll be able to pull off the same tricks yourself.
Ready? Popcorn 🍿 in hand? Let’s dive in.
🎯 Part 1: Scopes, The Treasure Map 🗺️ of Variables
Imagine JavaScript as a mansion 🏰 full of rooms.
Each room is a scope (function, block, or module).
When you say x
, JS starts looking for it in the drawers of your current room.
If it doesn’t find it there, it goes room by room until it either finds x
… or screams ReferenceError
in despair.
This is called the Scope Chain.
👉 Example:
let x = "Global";
function outer() {
let x = "Enclosing";
function inner() {
let x = "Local";
console.log(x);
}
inner();
}
outer();
🖨️ Output:
Local
Behind the scenes, JS is doing this:
inner scope → outer scope → global scope
If it never finds x
, it gives up dramatically:
“I checked every room! Your socks are gone!” 🧦
🧩 Behind the Scenes: Lexical Scope
JS doesn’t search dynamically, it already knows the treasure map when the code is written.
This is called lexical scope: scopes are determined by where you wrote the code, not where you call it from.
🔄 Part 2: let
, const
and the Magic of Block Scope
JS used to have only var
, which is function-scoped.
Then ES6 came along and brought let
and const
, which are block-scoped.
if (true) {
let secret = "shh!";
console.log(secret); // ✅ works
}
console.log(secret); // ❌ ReferenceError
Block scope means even curly braces {}
create a private little drawer just for those variables.
🪢 Part 3: Closures, Backpacks 🎒 for Variables
Closures are the reason JS feels like a magician 🧙.
They let a function remember variables from its birth environment even after that environment is gone.
Think of closures as little backpacks 🎒 your function carries with it.
👉 Example:
function makeMultiplier(n) {
return function (x) {
return x * n;
};
}
const times3 = makeMultiplier(3);
const times5 = makeMultiplier(5);
console.log(times3(10)); // 30
console.log(times5(10)); // 50
Even though makeMultiplier
is done running,
times3
still remembers n = 3
.
That memory is stored inside its closure backpack.
🧠 How Closures Really Work
JS doesn’t take a snapshot of variables.
It keeps a live reference to them.
function outer() {
let arr = [1, 2, 3];
return function () {
return arr;
};
}
const func = outer();
console.log(func()); // [1, 2, 3]
func().push(4);
console.log(func()); // [1, 2, 3, 4]
The array arr
lives on, hidden inside the closure’s backpack.
Mutate it, and the inner function sees the change, because they share the same memory.
Closures aren’t copying values.
They’re keeping the drawers open forever.
🚨 Gotcha: Closures in Loops
Closures can also cause weird bugs if you’re not careful.
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
Output:
3
3
3
Because var
is function-scoped, all three callbacks share the same drawer.
By the time they run, i = 3
.
Solution: use let
(block scope creates a new drawer each time):
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
Now you get:
0
1
2
🧰 Closures in the Wild
Closures power half the magic in modern JS:
- Private variables (data hiding):
function createBank() {
let balance = 0;
return {
deposit(amount) {
balance += amount;
},
getBalance() {
return balance;
}
};
}
const bank = createBank();
bank.deposit(100);
console.log(bank.getBalance()); // 100
balance
is completely private, no outside code can touch it directly.
- Event handlers & callbacks
- Memoization & caching
- Factories for creating pre-configured functions
Closures = memory + function = endless possibilities.
🎭 Part 4: Function Wrappers (Poor Man’s Decorators)
JS doesn’t need a special keyword for decorators, functions can wrap other functions easily.
👉 Example:
function shout(func) {
return function (...args) {
return func(...args).toUpperCase();
};
}
function greet() {
return "hello world";
}
const loudGreet = shout(greet);
console.log(loudGreet()); // HELLO WORLD
You just dressed greet
in a new costume 🎭.
This is how middlewares, loggers, and React hooks work under the hood.
⏱️ Practical Example: Timing Any Function
function timer(func) {
return function (...args) {
const start = performance.now();
const result = func(...args);
const end = performance.now();
console.log(`${func.name} took ${(end - start).toFixed(2)}ms`);
return result;
};
}
function slowTask() {
for (let i = 0; i < 1e7; i++) {}
}
const timedSlowTask = timer(slowTask);
timedSlowTask();
JS wraps slowTask
in a closure that remembers func
, calls it, and logs the time.
🎭 Stage 2: Real Decorators (Future of JS)
Modern JS is adding official decorator syntax for classes and methods:
function log(target, key, descriptor) {
const original = descriptor.value;
descriptor.value = function (...args) {
console.log(`Calling ${key} with`, args);
return original.apply(this, args);
};
return descriptor;
}
class MathOps {
@log
add(a, b) {
return a + b;
}
}
const m = new MathOps();
console.log(m.add(2, 3)); // logs call + returns 5
This is the future: native syntax for what we already do with closures.
🧠 The Big Picture
- Scopes → The mansion’s treasure map 🗺️ (lexical search order).
- Closures → Backpacks 🎒 that keep variables alive even after their parents are gone.
- Function wrappers / decorators → Costume designers 🎭 that rewire functions while carrying their memory.
Once you master these three, you stop “using JavaScript” and start bending it to your will.
Top comments (0)