JavaScript closures and lexical scope fundamentals
// Basic closure example
function outer() {
const message = 'Hello from outer';
function inner() {
console.log(message); // Accesses outer variable
}
return inner;
}
const closure = outer();
closure(); // "Hello from outer"
// Counter with closure (data privacy)
function createCounter() {
let count = 0; // Private variable
return {
increment() {
count++;
return count;
},
decrement() {
count--;
return count;
},
getCount() {
return count;
}
};
}
const counter = createCounter();
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
console.log(counter.getCount()); // 2
// console.log(counter.count); // undefined - private!
// Multiple independent closures
const counter1 = createCounter();
const counter2 = createCounter();
counter1.increment(); // 1
counter2.increment(); // 1
counter2.increment(); // 2
console.log(counter1.getCount()); // 1 (independent)
// Closure in loops (classic problem)
// Wrong way
for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // Prints 3, 3, 3
}, 100);
}
// Fixed with IIFE
for (var i = 0; i < 3; i++) {
(function(j) {
setTimeout(function() {
console.log(j); // Prints 0, 1, 2
}, 100);
})(i);
}
// Modern fix with let
for (let i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // Prints 0, 1, 2
}, 100);
}
// Function factory with closures
function multiplier(factor) {
return function(number) {
return number * factor;
};
}
const double = multiplier(2);
const triple = multiplier(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15
// Private methods pattern
function BankAccount(initialBalance) {
let balance = initialBalance; // Private
// Private method
function validateAmount(amount) {
return typeof amount === 'number' && amount > 0;
}
// Public methods
this.deposit = function(amount) {
if (validateAmount(amount)) {
balance += amount;
return balance;
}
throw new Error('Invalid amount');
};
this.withdraw = function(amount) {
if (validateAmount(amount) && amount <= balance) {
balance -= amount;
return balance;
}
throw new Error('Invalid or insufficient amount');
};
this.getBalance = function() {
return balance;
};
}
const account = new BankAccount(100);
account.deposit(50); // 150
account.withdraw(30); // 120
console.log(account.getBalance()); // 120
// account.balance // undefined - private!
// Closure with event handlers
function setupButton(buttonId, message) {
const button = document.getElementById(buttonId);
button.addEventListener('click', function() {
console.log(message); // Closure over message
});
}
setupButton('btn1', 'Button 1 clicked');
setupButton('btn2', 'Button 2 clicked');
// Module pattern
const calculator = (function() {
// Private variables and functions
let result = 0;
function validateNumber(n) {
if (typeof n !== 'number') {
throw new Error('Not a number');
}
}
// Public API
return {
add(n) {
validateNumber(n);
result += n;
return this;
},
subtract(n) {
validateNumber(n);
result -= n;
return this;
},
multiply(n) {
validateNumber(n);
result *= n;
return this;
},
getResult() {
return result;
},
reset() {
result = 0;
return this;
}
};
})();
calculator.add(5).multiply(3).subtract(2);
console.log(calculator.getResult()); // 13
// Memoization with closure
function memoize(fn) {
const cache = {}; // Closure variable
return function(...args) {
const key = JSON.stringify(args);
if (key in cache) {
console.log('From cache');
return cache[key];
}
console.log('Calculating...');
const result = fn(...args);
cache[key] = result;
return result;
};
}
const expensiveSum = memoize((a, b) => {
// Simulate expensive operation
for (let i = 0; i < 1000000000; i++) {}
return a + b;
});
console.log(expensiveSum(5, 3)); // Calculating... 8
console.log(expensiveSum(5, 3)); // From cache 8
// Partial application
function partial(fn, ...fixedArgs) {
return function(...remainingArgs) {
return fn(...fixedArgs, ...remainingArgs);
};
}
function greet(greeting, name) {
return `${greeting}, ${name}!`;
}
const sayHello = partial(greet, 'Hello');
console.log(sayHello('Alice')); // "Hello, Alice!"
console.log(sayHello('Bob')); // "Hello, Bob!"
// Lexical scope example
const globalVar = 'global';
function outer() {
const outerVar = 'outer';
function middle() {
const middleVar = 'middle';
function inner() {
const innerVar = 'inner';
// Can access all outer scopes
console.log(innerVar); // 'inner'
console.log(middleVar); // 'middle'
console.log(outerVar); // 'outer'
console.log(globalVar); // 'global'
}
inner();
}
middle();
}
outer();
Closures allow functions to access variables from outer scopes even after outer function returns. I create closures when inner functions reference outer function variables. Lexical scope means functions look up variables where they're defined, not where they're called. Closures enable data privacy by encapsulating variables. The module pattern uses closures to create private state and public interfaces. Using closures for event handlers preserves access to surrounding context. Each closure maintains its own scope chain and variable bindings. Closures power callbacks, higher-order functions, and functional programming patterns. Understanding closure lifetime prevents memory leaks. The closure scope chain includes local scope, outer function scopes, and global scope.