Closure in JavaScript is a function that has access to variables from an outer scope even after the outer function has finished executing. A closure “remembers” the environment in which it was created.
function outerFunction(x) {
// Outer variable
return function innerFunction(y) {
return x + y; // Access to x from outer scope
};
}
const addFive = outerFunction(5);
console.log(addFive(3)); // 8
A closure is a combination of a function and the lexical environment in which that function was declared. This allows the function to access variables from the outer scope.
Key features of closures:
function createGreeting(name) {
const greeting = `Hello, ${name}!`;
return function() {
console.log(greeting); // Access to greeting from outer scope
};
}
const greetAlex = createGreeting("Alexander");
const greetMaria = createGreeting("Maria");
greetAlex(); // "Hello, Alexander!"
greetMaria(); // "Hello, Maria!"
function createMultiplier(multiplier) {
return function(number) {
return number * multiplier;
};
}
const double = createMultiplier(2);
const triple = createMultiplier(3);
const square = createMultiplier(4);
console.log(double(5)); // 10
console.log(triple(5)); // 15
console.log(square(5)); // 20
function createCounter(initialValue = 0) {
let count = initialValue;
return {
increment: () => ++count,
decrement: () => --count,
getValue: () => count,
reset: () => count = initialValue
};
}
const counter1 = createCounter();
const counter2 = createCounter(10);
console.log(counter1.increment()); // 1
console.log(counter1.increment()); // 2
console.log(counter2.increment()); // 11
console.log(counter1.getValue()); // 2
console.log(counter2.getValue()); // 11
function createBankAccount(initialBalance) {
let balance = initialBalance;
let transactionHistory = [];
function addTransaction(type, amount) {
transactionHistory.push({
type,
amount,
date: new Date(),
balance: balance
});
}
return {
deposit(amount) {
if (amount > 0) {
balance += amount;
addTransaction('deposit', amount);
return balance;
}
throw new Error('Amount must be positive');
},
withdraw(amount) {
if (amount > 0 && amount <= balance) {
balance -= amount;
addTransaction('withdraw', amount);
return balance;
}
throw new Error('Insufficient funds or invalid amount');
},
getBalance() {
return balance;
},
getHistory() {
return [...transactionHistory]; // Return a copy
}
};
}
const account = createBankAccount(1000);
console.log(account.deposit(500)); // 1500
console.log(account.withdraw(200)); // 1300
console.log(account.getBalance()); // 1300
// balance is not accessible directly!
// console.log(account.balance); // undefined
// WRONG - all functions will output 3
for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // 3, 3, 3
}, 100);
}
// Solution 1: Using let
for (let i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // 0, 1, 2
}, 100);
}
// Solution 2: IIFE (Immediately Invoked Function Expression)
for (var i = 0; i < 3; i++) {
(function(index) {
setTimeout(function() {
console.log(index); // 0, 1, 2
}, 100);
})(i);
}
// Solution 3: Factory function
function createLogger(value) {
return function() {
console.log(value);
};
}
for (var i = 0; i < 3; i++) {
setTimeout(createLogger(i), 100); // 0, 1, 2
}
function memoize(fn) {
const cache = new Map();
return function(...args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
console.log('From cache:', key);
return cache.get(key);
}
const result = fn.apply(this, args);
cache.set(key, result);
console.log('Computed:', key);
return result;
};
}
// Slow function for demonstration
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
const memoizedFib = memoize(fibonacci);
console.log(memoizedFib(10)); // Computed: [10]
console.log(memoizedFib(10)); // From cache: [10]
const Calculator = (function() {
let history = [];
let currentValue = 0;
function addToHistory(operation, value, result) {
history.push({
operation,
value,
result,
timestamp: Date.now()
});
}
return {
add(value) {
const result = currentValue + value;
addToHistory('add', value, result);
currentValue = result;
return this; // For method chaining
},
subtract(value) {
const result = currentValue - value;
addToHistory('subtract', value, result);
currentValue = result;
return this;
},
multiply(value) {
const result = currentValue * value;
addToHistory('multiply', value, result);
currentValue = result;
return this;
},
getValue() {
return currentValue;
},
getHistory() {
return [...history];
},
clear() {
currentValue = 0;
history = [];
return this;
}
};
})();
// Usage
Calculator
.add(10)
.multiply(2)
.subtract(5);
console.log(Calculator.getValue()); // 15
console.log(Calculator.getHistory());
function createApiClient(baseUrl, defaultHeaders = {}) {
const headers = { ...defaultHeaders };
function makeRequest(endpoint, options = {}) {
const url = `${baseUrl}${endpoint}`;
const requestHeaders = { ...headers, ...options.headers };
return fetch(url, {
...options,
headers: requestHeaders
});
}
return {
get(endpoint, options) {
return makeRequest(endpoint, { ...options, method: 'GET' });
},
post(endpoint, data, options) {
return makeRequest(endpoint, {
...options,
method: 'POST',
body: JSON.stringify(data),
headers: {
'Content-Type': 'application/json',
...options?.headers
}
});
},
setHeader(key, value) {
headers[key] = value;
},
removeHeader(key) {
delete headers[key];
}
};
}
const apiClient = createApiClient('https://api.example.com', {
'Authorization': 'Bearer token123'
});
apiClient.setHeader('X-Custom-Header', 'value');
// apiClient.get('/users');
// apiClient.post('/users', { name: 'John' });
// BAD - can lead to memory leaks
function attachListeners() {
const largeData = new Array(1000000).fill('data');
document.getElementById('button').addEventListener('click', function() {
// Closure holds reference to largeData
console.log('Clicked!');
});
}
// GOOD - avoid unnecessary references
function attachListeners() {
const largeData = new Array(1000000).fill('data');
function handleClick() {
console.log('Clicked!');
}
document.getElementById('button').addEventListener('click', handleClick);
// Clear reference if no longer needed
// largeData = null; // If data is no longer needed
}
// Create functions outside the loop
function createHandler(index) {
return function() {
console.log(`Handler ${index}`);
};
}
// BAD - creating functions in loop
const handlers1 = [];
for (let i = 0; i < 1000; i++) {
handlers1.push(function() {
console.log(`Handler ${i}`);
});
}
// BETTER - reuse factory function
const handlers2 = [];
for (let i = 0; i < 1000; i++) {
handlers2.push(createHandler(i));
}
function createFunctions() {
const functions = [];
for (var i = 0; i < 3; i++) {
functions.push(function() {
return i;
});
}
return functions;
}
const funcs = createFunctions();
console.log(funcs[0]()); // ?
console.log(funcs[1]()); // ?
console.log(funcs[2]()); // ?
3
, 3
, 3
All functions reference the same variable i
, which equals 3 after the loop completes.
Fix:
for (let i = 0; i < 3; i++) { // let instead of var
functions.push(function() {
return i;
});
}
// Create a function that returns an object with methods
// start(), stop(), getTime()
// Timer should count seconds
function createTimer() {
// Your code here
}
const timer = createTimer();
timer.start();
// After 3 seconds
console.log(timer.getTime()); // ~3
timer.stop();
function createTimer() {
let startTime = null;
let elapsedTime = 0;
let isRunning = false;
let intervalId = null;
return {
start() {
if (!isRunning) {
startTime = Date.now() - elapsedTime;
isRunning = true;
}
},
stop() {
if (isRunning) {
elapsedTime = Date.now() - startTime;
isRunning = false;
}
},
getTime() {
if (isRunning) {
return Math.floor((Date.now() - startTime) / 1000);
}
return Math.floor(elapsedTime / 1000);
},
reset() {
startTime = null;
elapsedTime = 0;
isRunning = false;
}
};
}
// Create a function that can be called only n times
function createLimitedFunction(fn, limit) {
// Your code here
}
const limitedLog = createLimitedFunction(console.log, 3);
limitedLog('Call 1'); // Will output
limitedLog('Call 2'); // Will output
limitedLog('Call 3'); // Will output
limitedLog('Call 4'); // Won't output
function createLimitedFunction(fn, limit) {
let callCount = 0;
return function(...args) {
if (callCount < limit) {
callCount++;
return fn.apply(this, args);
}
console.log(`Function can only be called ${limit} time(s)`);
};
}
// Create a function that caches computation results
function createCachedFunction(fn, maxCacheSize = 10) {
// Your code here
}
function expensiveOperation(n) {
console.log(`Computing for ${n}`);
return n * n;
}
const cached = createCachedFunction(expensiveOperation, 3);
console.log(cached(5)); // Computing for 5, returns 25
console.log(cached(5)); // From cache, returns 25
function createCachedFunction(fn, maxCacheSize = 10) {
const cache = new Map();
return function(...args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
return cache.get(key);
}
const result = fn.apply(this, args);
// If cache is full, remove oldest element
if (cache.size >= maxCacheSize) {
const firstKey = cache.keys().next().value;
cache.delete(firstKey);
}
cache.set(key, result);
return result;
};
}
function mystery() {
let x = 1;
function inner() {
console.log(x);
let x = 2;
}
inner();
}
mystery();
ReferenceError: Cannot access 'x' before initialization
Due to hoisting, the variable x
is declared in the inner
function, but access occurs before initialization (temporal dead zone).
function createAsyncCounter() {
let count = 0;
return {
async increment(delay = 1000) {
await new Promise(resolve => setTimeout(resolve, delay));
return ++count;
},
async getCount() {
return count;
}
};
}
const asyncCounter = createAsyncCounter();
// Usage
(async () => {
console.log(await asyncCounter.increment()); // 1 (after 1 sec)
console.log(await asyncCounter.increment(500)); // 2 (after 0.5 sec)
console.log(await asyncCounter.getCount()); // 2
})();
function createSequenceGenerator(start = 0, step = 1) {
let current = start;
return function* () {
while (true) {
yield current;
current += step;
}
};
}
const evenNumbers = createSequenceGenerator(0, 2)();
const oddNumbers = createSequenceGenerator(1, 2)();
console.log(evenNumbers.next().value); // 0
console.log(evenNumbers.next().value); // 2
console.log(oddNumbers.next().value); // 1
console.log(oddNumbers.next().value); // 3
// ERROR: all handlers reference the last value
const buttons = document.querySelectorAll('button');
for (var i = 0; i < buttons.length; i++) {
buttons[i].onclick = function() {
alert('Button ' + i); // Always the last i
};
}
// FIX: use let or closure
for (let i = 0; i < buttons.length; i++) {
buttons[i].onclick = function() {
alert('Button ' + i); // Correct i
};
}
// PROBLEM: closure holds reference to large object
function createHandler(largeObject) {
return function(event) {
// Using only one property
console.log(largeObject.id);
};
}
// SOLUTION: extract only needed data
function createHandler(largeObject) {
const id = largeObject.id; // Copy only what's needed
return function(event) {
console.log(id);
};
}
// What will this output?
function test() {
console.log(a); // ?
console.log(b); // ?
var a = 1;
let b = 2;
}
test();
// a: undefined (hoisting)
// b: ReferenceError (temporal dead zone)
// Good: private data
function createUser(name, email) {
// Private variables
let _name = name;
let _email = email;
let _isActive = true;
return {
getName: () => _name,
getEmail: () => _email,
isActive: () => _isActive,
deactivate: () => _isActive = false,
// Validation on change
setEmail: (newEmail) => {
if (newEmail.includes('@')) {
_email = newEmail;
} else {
throw new Error('Invalid email');
}
}
};
}
// Bad: unnecessary closure
function processArray(arr) {
return arr.map(function(item) {
return item * 2;
});
}
// Better: simple function
function double(item) {
return item * 2;
}
function processArray(arr) {
return arr.map(double);
}
/**
* Creates a function with call frequency limitation (throttle)
* @param {Function} fn - Function to limit
* @param {number} delay - Delay in milliseconds
* @returns {Function} Limited function
*/
function createThrottledFunction(fn, delay) {
let lastCallTime = 0;
let timeoutId = null;
return function(...args) {
const now = Date.now();
if (now - lastCallTime >= delay) {
lastCallTime = now;
fn.apply(this, args);
} else if (!timeoutId) {
timeoutId = setTimeout(() => {
lastCallTime = Date.now();
timeoutId = null;
fn.apply(this, args);
}, delay - (now - lastCallTime));
}
};
}
Closures are a powerful JavaScript mechanism that allows:
✅ Creating private variables and methods
✅ Preserving state between function calls
✅ Implementing patterns like module and factory
✅ Creating specialized functions (currying, memoization)
✅ Managing variable scope
Important to remember:
let
and const
behave differently in loopsModern alternatives:
Closures are the foundation of understanding JavaScript. Master them well, and you’ll be able to write more elegant and functional code!
Want more interview preparation articles? Subscribe to EasyAdvice, bookmark the site and improve yourself every day 💪