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

Why does this work?

  • outer() has finished executing.
  • But inner still remembers count — 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()); // 100

No 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)

ConceptMeaning
Lexical ScopeScope decided by where functions are written.
ClosureFunction + 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 bar is 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:

  • this
  • arguments
  • super
  • new.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 10

Why?
➡️ 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()(); // 10

Interaction of this + Closures

Very important:

this is NOT part of closure.
this is NOT a variable stored in lexical scope.

Closures store only:

  • let
  • const
  • var
  • 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:

  • inner closure → captures x=10
  • inner this → equals obj (because of obj.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)

ConceptRegular FunctionArrow 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 behaviorSameSame (no difference)

Closures don’t care about function type.

Arrow or regular — both form closures.

Arrow functions capture this lexically; regular functions don’t.

Named functions don’t change closure behavior; they only help debugging.