Scope and Closures
Scope determines where variables are accessible. Closures allow functions to remember variables from their outer scope even after that outer function has returned. Together they underpin JavaScript’s most powerful patterns.
Types of Scope
Global Scope
Variables declared outside any function or block:
let globalVar = 'I am global';
function showGlobal() {
console.log(globalVar);
}
showGlobal(); // 'I am global'
Avoid polluting global scope — use modules or IIFEs in legacy code.
Function Scope
var is function-scoped — visible throughout the entire function:
function example() {
var fnScoped = 'inside function';
if (true) {
var alsoFnScoped = 'still function scope';
}
console.log(alsoFnScoped); // accessible
}
// console.log(fnScoped); // ReferenceError
Block Scope
let and const are block-scoped — limited to nearest {}:
if (true) {
let blockVar = 'block';
const alsoBlock = 'block';
}
// console.log(blockVar); // ReferenceError
for (let i = 0; i < 3; i++) {
// each iteration gets its own `i`
}
Best practice: Use const by default; let when reassignment needed; avoid var.
Temporal Dead Zone (TDZ)
let/const exist but are inaccessible before their declaration line:
// console.log(x); // ReferenceError — TDZ
let x = 10;
var is hoisted and initialized to undefined — another reason to avoid it.
Lexical Scope
JavaScript uses lexical (static) scope — a function’s scope is determined by where it is written, not where it is called:
let outer = 'outer';
function outerFn() {
let inner = 'inner';
function innerFn() {
console.log(outer, inner); // both accessible
}
return innerFn;
}
const fn = outerFn();
fn(); // 'outer', 'inner' — closure preserves inner
Closures
A closure is a function plus its surrounding lexical environment:
function createCounter() {
let count = 0;
return function() {
count++;
return count;
};
}
const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3
const counter2 = createCounter();
console.log(counter2()); // 1 — independent closure
Each call to createCounter() creates a new closure with its own count.
Practical Closure Uses
Module Pattern (Private State)
function createBankAccount(initialBalance) {
let balance = initialBalance;
return {
deposit(amount) {
balance += amount;
return balance;
},
withdraw(amount) {
if (amount <= balance) balance -= amount;
return balance;
},
getBalance() {
return balance;
}
};
}
const account = createBankAccount(100);
account.deposit(50); // 150
account.withdraw(30); // 120
// account.balance — undefined (private)
Factory Functions
function multiply(factor) {
return (number) => number * factor;
}
const double = multiply(2);
const triple = multiply(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15
Event Handlers with State
function setupButton(label) {
let clicks = 0;
return function handleClick() {
clicks++;
console.log(`${label}: ${clicks} clicks`);
};
}
document.querySelector('#btn').addEventListener('click', setupButton('Submit'));
Closures in Loops (Common Pitfall)
// Problem: var is function-scoped — all callbacks share same i
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100); // 3, 3, 3
}
// Fix 1: use let (block scope per iteration)
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100); // 0, 1, 2
}
// Fix 2: IIFE captures current i
for (var i = 0; i < 3; i++) {
((j) => setTimeout(() => console.log(j), 100))(i);
}
Scope Chain
When a variable is referenced, JavaScript searches the scope chain:
- Current function/block scope
- Outer function scope
- … up to global
- ReferenceError if not found
let a = 1;
function outer() {
let b = 2;
function inner() {
let c = 3;
console.log(a, b, c); // 1, 2, 3
}
inner();
}
Inner scope can read outer variables; outer scope cannot read inner variables.
Closures and Memory
Closures keep outer variables alive as long as the inner function is reachable. This can cause memory leaks if closures hold large objects unnecessarily:
function leak() {
const hugeData = new Array(1e6).fill('x');
return function() {
console.log(hugeData.length); // keeps hugeData in memory
};
}
Release references when done: handler = null.
IIFE (Immediately Invoked Function Expression)
Legacy pattern for private scope before modules:
(function() {
const private = 'hidden';
window.myApp = { getPrivate: () => private };
})();
Modern code uses ES modules instead.
Best Practices
- Prefer
const/letovervar - Keep closures small — return only what’s needed
- Watch loop + async patterns — use
letorfor...of - Use modules for encapsulation in modern projects
Troubleshooting
Unexpected undefined in closure
- Variable changed before async callback runs — capture value in loop with
letor parameter
Memory usage growing
- Closures holding DOM references to removed elements — null out references
this behaves unexpectedly in closure
- Arrow functions inherit lexical
this; regular functions have ownthis
Understanding scope and closures is essential for callbacks, async code, modules, and advanced JavaScript patterns.