Scopes, Closures, and Decorators in JavaScript Explained 🧠
Hey JavaScript developers! 👋
Have you ever wondered:
- Why can an inner function access variables from its parent function?
- How does JavaScript know where to find a variable?
- Why do closures seem to "remember" values forever?
- How do decorators magically add new behavior to existing functions?
If you've ever asked any of these questions, you're about to unlock one of the most important pieces of JavaScript.
Today we're diving into:
- Scopes
- Closures
- Decorators
And more importantly, what happens behind the scenes inside the JavaScript engine.
Ready?
Let's pull back the curtain. 🎭
The Castle of Variables: Understanding Scope
Every variable in JavaScript lives somewhere.
That "somewhere" is called its scope.
Scope determines:
- Who can access a variable
- Where a variable exists
- When a variable gets destroyed
Think of scope like rooms inside a castle.
Some variables live in public areas.
Some live in private rooms.
Some are only accessible inside tiny hidden chambers.
JavaScript Uses Lexical Scope
JavaScript is a lexically scoped language.
This means:
Where you write the code determines what variables are accessible.
Not where the function is called.
Example:
const globalVar = "🌎";
function outer() {
const outerVar = "🚪";
function inner() {
console.log(globalVar);
console.log(outerVar);
}
inner();
}
outer();
Output:
🌎
🚪
Why?
Because inner() was written inside outer().
It automatically gains access to everything in its surrounding scope.
The Four Levels of Scope
Global Scope 🌍
Variables declared outside any function or block.
const siteName = "My App";
Accessible everywhere.
Function Scope 🏛️
Every function creates its own scope.
function greet() {
const message = "Hello";
}
Outside code cannot access message.
Block Scope 🚪
Created by:
{
}
When using:
let
const
Example:
if (true) {
let secret = "Hidden";
}
console.log(secret);
Output:
ReferenceError
Module Scope 📦
Modern ES Modules create their own top-level scope.
export const apiUrl = "...";
Variables don't leak into the global namespace.
This is one reason modern JavaScript is much cleaner.
Behind the Scenes: Execution Context
Whenever JavaScript executes code, it creates an Execution Context.
Think of it as a workspace for the currently running code.
Every execution context contains:
Variable Environment
Where variables live.
Scope Chain
References to outer scopes.
This Binding
The current value of this.
Example:
function outer() {
const outerVar = "🚪";
function inner() {
const innerVar = "🗝️";
console.log(outerVar);
}
inner();
}
When inner() executes:
inner Context
↓
outer Context
↓
Global Context
This chain is called the Scope Chain.
Closures: Functions That Remember
Now things get interesting.
A closure is created when a function remembers variables from its surrounding scope even after that scope has finished executing.
Let's see it.
function counter() {
let count = 0;
return function increment() {
count++;
console.log(count);
};
}
const add = counter();
add();
add();
add();
Output:
1
2
3
Wait...
counter() already finished executing.
Why is count still available?
That's exactly what a closure is.
Closures Are Like Memory Jars 🫙
Imagine the inner function carries a jar containing all the variables it still needs.
When counter() finishes:
count = 0
doesn't disappear.
Instead:
increment()
↓
Closure Memory
↓
count
As long as increment exists, the closure survives.
Behind the Scenes: How Closures Work
Let's break it down.
Step 1
counter() executes.
A new execution context is created.
count = 0
lives inside it.
Step 2
increment() is returned.
Normally the execution context would be destroyed.
But...
Step 3
JavaScript notices that increment() still references count.
So the engine preserves the variable.
This preserved environment becomes the closure scope.
Step 4
Each time:
add();
runs, it accesses the same stored count.
Garbage Collection and Closures
Closures are incredibly useful.
But they affect memory.
JavaScript's garbage collector can only remove closure data when:
add = null;
or no references remain.
Until then:
count
stays alive.
Real-World Closure Example
Creating personalized greeters:
function createGreeter(name) {
return function() {
console.log(
`Hello ${name}`
);
};
}
const greetAnik =
createGreeter("Anik");
greetAnik();
Output:
Hello Anik
The closure remembers:
name = "Anik"
even though createGreeter() has already finished.
What Are Decorators?
Now let's level up.
A decorator is a function that wraps another function and adds new behavior.
Think of decorators like giving a function superpowers.
The original function remains unchanged.
The decorator simply enhances it.
Your First Decorator
function logDecorator(fn) {
return function(...args) {
console.log(
`Calling ${fn.name}`
);
return fn(...args);
};
}
function greet(name) {
return `Hello ${name}`;
}
const decorated =
logDecorator(greet);
console.log(
decorated("Anik")
);
Output:
Calling greet
Hello Anik
Pretty neat.
How Decorators Work Internally
Notice something?
function logDecorator(fn) {
return function(...args) {
return fn(...args);
};
}
The inner function references:
fn
from the outer scope.
That means...
The decorator is actually using a closure.
Decorators Are Built on Closures
This relationship is important:
Scope
↓
Closure
↓
Decorator
Scopes make closures possible.
Closures make decorators possible.
These concepts build on each other.
Practical Decorator: Logging
function logger(fn) {
return function(...args) {
console.log(
`Calling ${fn.name}`
);
return fn(...args);
};
}
Useful for:
- Debugging
- Monitoring
- Analytics
- Observability
Practical Decorator: Timing Functions
function timer(fn) {
return function(...args) {
const start =
performance.now();
const result =
fn(...args);
const end =
performance.now();
console.log(
`Took ${end-start}ms`
);
return result;
};
}
Great for performance profiling.
Practical Decorator: Debouncing
One of the most common frontend patterns.
function debounce(
fn,
delay
) {
let timer;
return function(...args) {
clearTimeout(timer);
timer = setTimeout(
() => fn(...args),
delay
);
};
}
Used heavily in:
- Search boxes
- Window resize events
- Scroll handlers
Notice how the closure remembers:
timer
between calls.
Common Closure Pitfall
Closures capture references.
Not copies.
Example:
for (
var i = 0;
i < 3;
i++
) {
setTimeout(
() => console.log(i),
100
);
}
Output:
3
3
3
Why?
All callbacks reference the same i.
Fix Using Block Scope
for (
let i = 0;
i < 3;
i++
) {
setTimeout(
() => console.log(i),
100
);
}
Output:
0
1
2
Each iteration gets its own scope.
Hoisting and Scope
JavaScript hoists declarations.
Example:
console.log(a);
var a = 10;
Output:
undefined
Internally:
var a;
console.log(a);
a = 10;
But:
console.log(b);
let b = 10;
Produces:
ReferenceError
Because of the Temporal Dead Zone (TDZ).
Putting It All Together
Scope
Determines where variables live.
Closure
Allows functions to remember variables.
Decorator
Uses closures to enhance functions.
These three concepts form the foundation of modern JavaScript architecture.
TL;DR Quick Recap
- JavaScript uses lexical scope.
- Variables live inside scopes.
- Execution contexts manage variables and scope chains.
- Closures preserve variables after a function finishes.
- Closures survive until garbage collection removes them.
- Decorators wrap functions with additional behavior.
- Decorators are powered by closures.
- Many frontend patterns rely on closures and decorators.
Frequently Asked Questions
What is scope in JavaScript?
Scope determines where variables are accessible.
JavaScript supports:
- Global Scope
- Function Scope
- Block Scope
- Module Scope
What is lexical scope?
Lexical scope means variable access is determined by where code is written, not where functions are called.
What is a closure?
A closure is a function that remembers variables from its surrounding scope even after that scope has finished executing.
Why are closures useful?
Closures allow developers to:
- Preserve state
- Create private variables
- Build factories
- Implement memoization
- Build decorators
Can closures cause memory leaks?
Yes.
If closures hold references to large objects that are never released, memory usage can grow unnecessarily.
What is an execution context?
An execution context is the environment JavaScript creates when executing code.
It contains:
- Variables
- Scope references
- This binding
What is a decorator in JavaScript?
A decorator is a function that wraps another function or class and extends its behavior.
Are decorators part of JavaScript?
Decorators are commonly used through patterns and are heavily supported in TypeScript and modern frameworks.
How do decorators work?
Decorators return a new function that calls the original function while adding extra functionality.
Why do decorators rely on closures?
The wrapper function must remember the original function being decorated.
Closures make that possible.
What frameworks use decorators?
Examples include:
- Angular
- NestJS
- MobX
- TypeScript ecosystems
Decorator-like patterns also appear throughout React and Node.js applications.
What is the scope chain?
The scope chain is the sequence of scopes JavaScript searches when resolving variables.
Local
↓
Outer
↓
Global
What is hoisting?
Hoisting is JavaScript's behavior of moving declarations to the top of their scope before execution.
Why does var behave differently from let?
var is function-scoped.
let is block-scoped and respects the Temporal Dead Zone.
Key Takeaways
- JavaScript uses lexical scope.
- Execution contexts manage variable environments.
- Closures preserve variables after execution.
- Closures enable private state and powerful abstractions.
- Decorators enhance functions without modifying them.
- Decorators rely on closures internally.
- Understanding these concepts is essential for mastering JavaScript.
Final Thoughts
Scopes, closures, and decorators are some of the most important concepts in JavaScript.
At first they may seem like separate topics.
But they're deeply connected.
Scopes create environments.
Closures preserve environments.
Decorators leverage closures to enhance behavior.
Once you truly understand these three concepts, JavaScript starts feeling less like magic and more like a beautifully designed system.
And that's when your code starts leveling up. 🚀
About the Author
Anik Sikder is a Software Engineer specializing in Frontend Development, Backend Systems, Cloud Infrastructure, Software Architecture, and Modern Web Technologies.
He writes about JavaScript, TypeScript, Python, Django, FastAPI, distributed systems, DevOps, and scalable software engineering practices.



