1. What is Lexical Scope?
Lexical scope means scope is decided by where variables are written in the source code, not where functions are called.
Think of it like a family tree:
- A child can access things from the parent’s home.
- But a parent cannot access things from the child’s room.
JavaScript uses lexical (static) scoping, not dynamic scoping.
Example:
function outer() {
let a = 10;
function inner() {
console.log(a);
}
inner();
}
outer();Why does inner() get a?
➡️ Because inner is created inside outer, so it has access to variables defined in outer.
Key point:
Where a function is written decides what variables it can access — even if it’s called from somewhere else.
2. How JavaScript Builds Lexical Scope?
When code is parsed, JavaScript creates Scope Chains.
Every function gets its own scope, and it also gets a link to its parent’s scope (where it was defined).
So inner → can look inside its own scope → if not found → it checks outer’s scope → then global.
This chain is known as the Lexical Environment.
3. What is a Closure?
A closure is created when a function “remembers” its lexical scope even after the outer function has finished executing.
Or in simple words:
Closure = Function + its lexical (parent) scope that is preserved.
Even if the parent function is gone from the call stack, the inner function can still use its variables.
Example:
function outer() {
let count = 0;
return function inner() {
count++;
console.log(count);
}
}
const counter = outer();
counter(); // 1
counter(); // 2
counter(); // 3Why does this work?
outer()has finished executing.- But
innerstill rememberscount— thanks to closure.
4. Why Closures Are Powerful?
Closures enable:
✔️ Data privacy (encapsulation)
function createBank() {
let balance = 0;
return {
deposit(amount) { balance += amount; },
getBalance() { return balance; }
};
}
const bank = createBank();
bank.deposit(100);
console.log(bank.getBalance()); // 100No one can directly modify balance.
✔️ Callbacks & Async code
In async operations, closures preserve variables.
for (var i = 1; i <= 3; i++) {
setTimeout(() => console.log(i), 1000);
}Output:
4
4
4
Because all callbacks share same lexical scope of i.
Using let fixes it:
for (let i = 1; i <= 3; i++) {
setTimeout(() => console.log(i), 1000);
}Now output:
1
2
3
✔️ Function factories
function multiply(x) {
return function(y) {
return x * y;
}
}
const double = multiply(2);
console.log(double(5)); // 10🔹 5. How Closures Actually Work Internally? (Deep concept)
When outer() executes, JavaScript creates a closure record that stores variables used by inner functions.
The memory is not freed until all inner functions referencing it are garbage collected.
So closure prevents variables from being destroyed.
🔹 6. Common Misconception
❌ Closure is not created only when you return a function.
✔️ Closure is created whenever a function accesses outer variables — return is not required.
Example:
function outer() {
let x = 10;
function inner() {
console.log(x);
}
inner(); // closure still exists
}
outer();🔹 Summary (Short & Sweet)
| Concept | Meaning |
|---|---|
| Lexical Scope | Scope decided by where functions are written. |
| Closure | Function + preserved lexical scope even after outer function finishes. |
| Why? | For data privacy, async code, factory functions, caching, currying. |
Closures With Named vs Unnamed (Anonymous) Functions
🔹 1. Named Function Expression
const foo = function bar() {
console.log("I am bar");
};Important points:
- The function name
baris only available inside the function itself. - Closure behavior does NOT depend on named/unnamed aspect.
Example:
function outer() {
let x = 10;
const fn = function bar() {
console.log(x);
};
return fn;
}
outer()() // 10➡️ Closure works normally.
🔹 2. Anonymous Function Expression
const fn = function() {
console.log("hello");
}- Anonymous functions behave exactly like named ones in terms of closures.
- Name helps debugging stack traces but NOT closure generation.
Closures With Arrow Functions
Arrow functions (⇒) also capture lexical scope exactly like normal functions. BUT… They have two big differences:
✅ 1. Arrow functions capture this lexically
Arrow functions DO NOT have their own:
thisargumentssupernew.target
They instead “borrow” these from the surrounding lexical scope. This is the biggest nuance.
Example:
const obj = {
value: 10,
method: function() {
setTimeout(() => {
console.log(this.value);
}, 1000);
}
};
obj.method(); // prints 10Why?
➡️ Because the arrow function takes this from method()’s lexical scope (obj).
Compare with regular function:
const obj = {
value: 10,
method: function() {
setTimeout(function() {
console.log(this.value);
}, 1000);
}
};
obj.method(); // undefined (in strict mode)Here, this = global object (window / undefined in Node strict mode).
❗ Arrow functions CANNOT be used as constructors
const A = () => {};
new A(); // ❌ TypeError❗ Arrow functions don’t have their own arguments object
They use the parent’s arguments.
Closures with arrow functions work exactly the same:
function outer() {
let x = 10;
return () => {
console.log(x);
};
}
outer()(); // 10Interaction of this + Closures
Very important:
this is NOT part of closure.
this is NOT a variable stored in lexical scope.
Closures store only:
letconstvar- function declarations
But this is resolved:
- by call-site (normal function)
- by lexical scope (arrow function)
Regular function + closure
Here, closure works for variables, but this depends on call-site:
function outer() {
let x = 10;
return function inner() {
console.log(x); // closure
console.log(this); // call-site decides
};
}
const obj = { fn: outer() };
obj.fn(); Output:
10
{ fn: [Function: inner] }
Because:
innerclosure → captures x=10innerthis → equals obj (because ofobj.fn())
Arrow function + closure
function outer() {
let x = 10;
return () => {
console.log(x);
console.log(this);
};
}
const obj = { fn: outer() };
obj.fn();Output:
10
globalThis / undefined (from outer's this)
Because arrow function does NOT bind this.
It uses outer’s this, not obj’s.
Named Functions + Hoisting Nuances + Closures
Named functions are hoisted:
outer(); // OK
function outer() {
console.log("hello");
}But function expressions are NOT hoisted:
outer(); // ❌ Cannot access before initialization
const outer = function() {
console.log("hello");
};This affects closure when building factories:
function a() {
return b; // works only if b is declared before a is *executed*
}
const b = function() { ... };So named/unnamed or hoisting does not change closure itself, but changes availability.
Nuances With var, let, and const Inside Closures
var is function-scoped
So loops with var cause classic closure problems:
for (var i = 1; i <= 3; i++) {
setTimeout(() => console.log(i), 1000);
}Output:
4
4
4
Why?
All callbacks share the same i.
let is block-scoped
Each iteration gets its own i.
for (let i = 1; i <= 3; i++) {
setTimeout(() => console.log(i), 1000);
}Output:
1
2
3
Closures With Immediately Invoked Functions (IIFE)
Classic interview topic:
for (var i = 1; i <= 3; i++) {
(function(i) {
setTimeout(() => console.log(i), 1000);
})(i);
}Here IIFE creates a new lexical environment per iteration.
Summary Table (Gold for Interviews)
| Concept | Regular Function | Arrow Function |
|---|---|---|
Has own this? | Yes | ❌ No (lexical this) |
Has own arguments? | Yes | ❌ No |
| Can be a constructor? | ✔️ Yes | ❌ No |
| Best for object methods | ✔️ Yes | ❌ Usually no |
| Best for callbacks | 😐 Yes | ✔️ Very good |
| Closure behavior | Same | Same (no difference) |
Closures don’t care about function type.
Arrow or regular — both form closures.