Closures and Callbacks
Learn how closures retain lexical environments, scoping rules, lexical scope, and passing callback functions for async operations.
Downloaded from PrepIQ (https://prepiq.online)
Q1. What is a closure in JavaScript?
A closure is a function that has access to variables from another function's scope. This is possible because functions in JavaScript form closures around the data they need, and they retain access to outer function variables even after the outer function has returned.
// Simple closure example
function outer() {
let count = 0; // outer function's variable
function inner() {
count++; // inner function accesses outer variable
console.log(count);
}
return inner;
}
const counter = outer();
counter(); // 1
counter(); // 2
counter(); // 3
// The inner function 'closes over' the count variable
// It retains access to count even after outer() has finished executing
// Practical example: Counter factory
function createCounter() {
let count = 0;
return {
increment: () => ++count,
decrement: () => --count,
getCount: () => count
};
}
const myCounter = createCounter();
console.log(myCounter.increment()); // 1
console.log(myCounter.increment()); // 2
console.log(myCounter.decrement()); // 1
console.log(myCounter.getCount()); // 1
// count is private - cannot access directly
console.log(myCounter.count); // undefined
// Another example: Bank account with closures
function createBankAccount(initialBalance) {
let balance = initialBalance;
return {
deposit: (amount) => {
balance += amount;
return `Balance: $${balance}`;
},
withdraw: (amount) => {
if (amount > balance) {
return 'Insufficient funds';
}
balance -= amount;
return `Balance: $${balance}`;
},
getBalance: () => balance
};
}
const account = createBankAccount(1000);
console.log(account.deposit(500)); // Balance: $1500
console.log(account.withdraw(200)); // Balance: $1300
console.log(account.getBalance()); // 1300
// Closures in loops - common interview question
const functions = [];
// Without closure (WRONG)
for (var i = 0; i < 3; i++) {
functions.push(() => console.log(i)); // 'var' is function-scoped
}
functions[0](); // 3 (not 0!)
functions[1](); // 3 (not 1!)
functions[2](); // 3 (not 2!)
// With closure (CORRECT) - using let
const correctFunctions = [];
for (let j = 0; j < 3; j++) {
correctFunctions.push(() => console.log(j)); // 'let' is block-scoped
}
correctFunctions[0](); // 0
correctFunctions[1](); // 1
correctFunctions[2](); // 2
// Alternative: IIFE to create closure
const iifeFunctions = [];
for (var k = 0; k < 3; k++) {
iifeFunctions.push((function(value) {
return () => console.log(value);
})(k));
}
iifeFunctions[0](); // 0
iifeFunctions[1](); // 1
iifeFunctions[2](); // 2Interview tip: Explain that closures are created every time a function is created. Mention the practical benefits: data privacy (encapsulation), function factories, and module pattern. The loops example is a classic interview question - explain why 'var' causes the issue and why 'let' solves it.
Q2. What is lexical scoping and how does it relate to closures?
Lexical scoping means that the accessibility of variables is determined by the position of the variables in the source code (where functions are defined), not where they are called. This is also called static scoping.
// Lexical scoping example
let globalVar = 'global';
function outer() {
let outerVar = 'outer';
function middle() {
let middleVar = 'middle';
function inner() {
let innerVar = 'inner';
// inner() has access to all variables in its scope chain
console.log(innerVar); // 'inner' - own scope
console.log(middleVar); // 'middle' - parent scope
console.log(outerVar); // 'outer' - grandparent scope
console.log(globalVar); // 'global' - global scope
}
inner();
// console.log(innerVar); // ReferenceError - innerVar not accessible
}
middle();
}
outer();
// Scope chain visualization
// inner's scope chain: inner -> middle -> outer -> global
// Lexical scoping is determined at WRITE TIME, not at CALL TIME
let name = 'John';
function sayName() {
console.log(name); // John (lexical scope)
}
function callFromDifferentScope() {
let name = 'Jane';
sayName(); // Still prints 'John' because sayName looks to ITS lexical scope
}
callFromDifferentScope(); // John
// Dynamic scoping (hypothetical) would print 'Jane'
// But JavaScript uses lexical scoping
// How closures use lexical scoping
function createMultiplier(multiplier) {
// Function returns another function
// The returned function 'closes over' multiplier
// It remembers multiplier from its lexical scope
return function(number) {
return number * multiplier; // Uses multiplier from outer scope
};
}
const double = createMultiplier(2);
const triple = createMultiplier(3);
console.log(double(5)); // 10 - each closure 'remembers' its own multiplier
console.log(triple(5)); // 15
// Real-world: Event handlers and closures
const buttons = document.querySelectorAll('button');
buttons.forEach((button, index) => {
button.addEventListener('click', function() {
// Due to lexical scoping, this closure remembers the index
console.log(`Button ${index} clicked`);
});
});
// Without proper lexical scoping (before let/const)
const handlersWrong = [];
for (var i = 0; i < 3; i++) {
handlersWrong[i] = function() {
console.log(`Handler ${i}`); // All will log 'Handler 3'
};
}
// With lexical scoping (using let)
const handlersRight = [];
for (let j = 0; j < 3; j++) {
handlersRight[j] = function() {
console.log(`Handler ${j}`); // 0, 1, 2
};
}Interview tip: Emphasize that lexical scoping is determined at WRITE TIME (where the function is defined), not at CALL TIME (where it's called). This is why closures work - they capture variables from their lexical scope. Contrast with dynamic scoping (which some languages use) to show the difference.
Q3. What is a callback function and what are common use cases?
A callback function is a function that is passed as an argument to another function, and the receiving function will 'call back' (execute) the callback at some point during its execution.
// Simple callback example
function greet(name, callback) {
console.log('Hello ' + name);
callback(); // 'calling back' the callback function
}
function sayGoodbye() {
console.log('Goodbye!');
}
greet('John', sayGoodbye);
// Output:
// Hello John
// Goodbye!
// Callback with arguments
function add(a, b, callback) {
const result = a + b;
callback(result); // Passing result to callback
}
add(5, 3, function(sum) {
console.log('Sum is: ' + sum); // Sum is: 8
});
// Array methods with callbacks
const numbers = [1, 2, 3, 4, 5];
// forEach callback
numbers.forEach(function(num) {
console.log(num * 2); // 2, 4, 6, 8, 10
});
// map callback
const doubled = numbers.map(function(num) {
return num * 2;
}); // [2, 4, 6, 8, 10]
// filter callback
const evenNumbers = numbers.filter(function(num) {
return num % 2 === 0; // [2, 4]
});
// Asynchronous callback - setTimeout
console.log('Start');
setTimeout(function() {
console.log('After 2 seconds');
}, 2000);
console.log('End');
// Output:
// Start
// End
// After 2 seconds
// Reading file with callback (Node.js)
const fs = require('fs');
fs.readFile('file.txt', 'utf8', function(err, data) {
if (err) {
console.log('Error reading file:', err);
} else {
console.log('File content:', data);
}
});
// AJAX request with callback
function fetchData(url, callback) {
fetch(url)
.then(response => response.json())
.then(data => callback(data))
.catch(error => callback(null, error));
}
fetchData('/api/users', function(data, error) {
if (error) {
console.log('Error:', error);
} else {
console.log('Users:', data);
}
});
// Callback hell / Pyramid of doom (PROBLEM)
getUser(userId, function(user) {
getOrders(user.id, function(orders) {
getOrderDetails(orders[0].id, function(details) {
getPayment(details.id, function(payment) {
console.log('Payment:', payment);
});
});
});
});
// Solution: Use Promises or async/await
// Modern approach with async/await
async function getPaymentInfo(userId) {
try {
const user = await getUser(userId);
const orders = await getOrders(user.id);
const details = await getOrderDetails(orders[0].id);
const payment = await getPayment(details.id);
return payment;
} catch (error) {
console.error('Error:', error);
}
}
// Higher-order function with callback
function doubleAsync(number, callback) {
setTimeout(function() {
callback(number * 2);
}, 1000);
}
doubleAsync(5, function(result) {
console.log('Result:', result); // Result: 10 (after 1 second)
});Interview tip: Mention that callbacks are fundamental to JavaScript's asynchronous programming model. Discuss callback hell (pyramid of doom) and why Promises and async/await are now preferred. Explain that callbacks are still used in array methods (map, filter, forEach) even though they're not asynchronous.
Q4. What is callback hell and how can you avoid it?
Callback hell (also called pyramid of doom) occurs when multiple nested callbacks are used, making the code hard to read and maintain. This is especially common in asynchronous operations.
// CALLBACK HELL - Hard to read
function getUserData(userId, callback) {
getUser(userId, function(err, user) {
if (err) {
callback(err);
} else {
getOrders(user.id, function(err, orders) {
if (err) {
callback(err);
} else {
getOrderDetails(orders[0].id, function(err, details) {
if (err) {
callback(err);
} else {
getPaymentInfo(details.id, function(err, payment) {
if (err) {
callback(err);
} else {
callback(null, {
user: user,
orders: orders,
details: details,
payment: payment
});
}
});
}
});
}
});
}
});
}
getUserData(123, function(err, result) {
if (err) {
console.log('Error:', err);
} else {
console.log('User Data:', result);
}
});
// SOLUTION 1: Named callback functions (modularity)
function handleUser(err, user) {
if (err) return handleError(err);
getOrders(user.id, handleOrders.bind(null, user));
}
function handleOrders(user, err, orders) {
if (err) return handleError(err);
getOrderDetails(orders[0].id, handleDetails.bind(null, user, orders));
}
function handleDetails(user, orders, err, details) {
if (err) return handleError(err);
getPaymentInfo(details.id, handlePayment.bind(null, user, orders, details));
}
function handlePayment(user, orders, details, err, payment) {
if (err) return handleError(err);
console.log({ user, orders, details, payment });
}
function handleError(err) {
console.log('Error:', err);
}
getUser(123, handleUser);
// SOLUTION 2: Promises - Much cleaner
function getUserDataPromise(userId) {
return getUser(userId)
.then(user => getOrders(user.id).then(orders => ({ user, orders })))
.then(data => getOrderDetails(data.orders[0].id)
.then(details => ({ ...data, details })))
.then(data => getPaymentInfo(data.details.id)
.then(payment => ({ ...data, payment })))
.catch(error => console.log('Error:', error));
}
getUserDataPromise(123).then(result => {
console.log('User Data:', result);
});
// SOLUTION 3: Promises with better readability
function getUserDataPromise2(userId) {
let userData = {};
return getUser(userId)
.then(user => {
userData.user = user;
return getOrders(user.id);
})
.then(orders => {
userData.orders = orders;
return getOrderDetails(orders[0].id);
})
.then(details => {
userData.details = details;
return getPaymentInfo(details.id);
})
.then(payment => {
userData.payment = payment;
return userData;
})
.catch(error => console.log('Error:', error));
}
// SOLUTION 4: async/await - Most readable (BEST)
async function getUserDataAsync(userId) {
try {
const user = await getUser(userId);
const orders = await getOrders(user.id);
const details = await getOrderDetails(orders[0].id);
const payment = await getPaymentInfo(details.id);
return { user, orders, details, payment };
} catch (error) {
console.log('Error:', error);
}
}
// Usage
getUserDataAsync(123).then(result => {
console.log('User Data:', result);
});
// Or with await
async function main() {
const result = await getUserDataAsync(123);
console.log('User Data:', result);
}
main();Interview tip: Show the progression from callback hell to modern solutions. Explain that while the code in callback hell works, it's hard to understand and maintain. Demonstrate how Promises flatten the nesting, and async/await makes it look like synchronous code. Mention that async/await is now the modern standard.
Q5. How do closures relate to the module pattern?
The module pattern uses closures to create private and public members. It's a way to encapsulate code and avoid polluting the global namespace.
// Basic module pattern
const calculator = (function() {
// Private variables
let result = 0;
// Private methods
const log = (value) => {
console.log(`Result: ${value}`);
};
// Public methods (exposed through return object)
return {
add: (x) => {
result += x;
log(result);
return this; // For method chaining
},
subtract: (x) => {
result -= x;
log(result);
return this;
},
multiply: (x) => {
result *= x;
log(result);
return this;
},
divide: (x) => {
result /= x;
log(result);
return this;
},
getResult: () => result,
reset: () => {
result = 0;
log(result);
return this;
}
};
})();
// Usage
calculator.add(5); // Result: 5
calculator.multiply(2); // Result: 10
calculator.subtract(3); // Result: 7
// Cannot access private members
console.log(calculator.result); // undefined
// Method chaining
calculator.reset().add(10).multiply(2).subtract(5); // Chainable
// More advanced module pattern with configuration
const userModule = (function() {
// Private variables
const users = [];
const apiUrl = 'https://api.example.com/users';
// Private methods
const validateUser = (user) => {
return user.name && user.email;
};
const findUserIndex = (id) => {
return users.findIndex(user => user.id === id);
};
// Public API
return {
// Add user
addUser: (user) => {
if (!validateUser(user)) {
throw new Error('Invalid user data');
}
users.push(user);
return user;
},
// Get all users
getUsers: () => {
return [...users]; // Return copy to prevent external modification
},
// Get user by id
getUser: (id) => {
const index = findUserIndex(id);
return index !== -1 ? { ...users[index] } : null;
},
// Update user
updateUser: (id, updatedData) => {
const index = findUserIndex(id);
if (index !== -1) {
users[index] = { ...users[index], ...updatedData };
return users[index];
}
return null;
},
// Delete user
deleteUser: (id) => {
const index = findUserIndex(id);
if (index !== -1) {
users.splice(index, 1);
return true;
}
return false;
}
};
})();
// Usage
userModule.addUser({ id: 1, name: 'John', email: 'john@gmail.com' });
userModule.addUser({ id: 2, name: 'Jane', email: 'jane@gmail.com' });
console.log(userModule.getUsers()); // [{ id: 1, ... }, { id: 2, ... }]
console.log(userModule.getUser(1)); // { id: 1, name: 'John', email: '...' }
// Private variables are protected
console.log(userModule.users); // undefined
console.log(userModule.apiUrl); // undefined
// Modern alternative: ES6 Classes (but module pattern still relevant)
class UserModule2 {
#users = []; // Private field
addUser(user) {
this.#users.push(user);
}
getUsers() {
return [...this.#users];
}
}
// Module pattern with namespace
const App = {};
App.calculator = (function() {
let result = 0;
return {
add: (x) => { result += x; return result; },
getResult: () => result
};
})();
App.utils = (function() {
return {
formatDate: (date) => date.toISOString(),
parseJSON: (str) => JSON.parse(str)
};
})();Interview tip: Explain that the module pattern is a way to organize code before ES6 modules. It provides privacy through closures. Mention modern alternatives like ES6 modules and classes with private fields, but emphasize that the module pattern is still widely used in legacy code and is a great example of closures in action.
Q6. What is the difference between callback and event listener?
Both callbacks and event listeners use similar mechanisms, but they have different contexts and purposes. An event listener is a type of callback specifically designed for responding to events.
// Callback - general purpose function passed as argument
function process(data, callback) {
// Do some processing
const result = data * 2;
// Call the callback with result
callback(result);
}
process(5, function(result) {
console.log('Result:', result); // Result: 10
});
// Event listener - specific to DOM events
const button = document.querySelector('button');
// Adding event listener
button.addEventListener('click', function(event) {
console.log('Button clicked!', event);
});
// Key differences:
// 1. Callbacks are called manually
// 2. Event listeners are called automatically by the browser
// Example: Manual callback
function fetchData(url, onSuccess, onError) {
// Simulating async operation
setTimeout(() => {
if (url) {
onSuccess({ id: 1, name: 'John' });
} else {
onError('Invalid URL');
}
}, 1000);
}
fetchData('/api/user',
function(data) { // onSuccess callback
console.log('Data:', data);
},
function(error) { // onError callback
console.log('Error:', error);
}
);
// Event listener - triggered by user action
const form = document.querySelector('form');
form.addEventListener('submit', function(event) {
event.preventDefault();
console.log('Form submitted!');
});
input.addEventListener('input', function(event) {
console.log('Input value:', event.target.value);
});
// Multiple callbacks vs Multiple event listeners
// Callbacks - must be passed
function task(onComplete) {
console.log('Task running');
onComplete();
}
task(() => console.log('Task 1 complete'));
// Event listeners - can add multiple
button.addEventListener('click', () => console.log('Handler 1'));
button.addEventListener('click', () => console.log('Handler 2'));
button.addEventListener('click', () => console.log('Handler 3'));
// All three will execute when button is clicked
// Event listener with event object
button.addEventListener('click', function(event) {
console.log('Event type:', event.type); // 'click'
console.log('Target:', event.target); // the button element
console.log('Timestamp:', event.timestamp); // when event occurred
});
// Removing event listeners
function handleClick() {
console.log('Clicked!');
}
button.addEventListener('click', handleClick);
// Later, remove the listener
button.removeEventListener('click', handleClick);
// Event delegation using event listener
const list = document.querySelector('ul');
list.addEventListener('click', function(event) {
if (event.target.tagName === 'LI') {
console.log('List item clicked:', event.target.textContent);
}
});
// Callback in promise chain
function getData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({ id: 1, name: 'John' });
}, 1000);
});
}
getData().then(function(data) { // then callback
console.log('Data:', data);
}).catch(function(error) { // catch callback
console.log('Error:', error);
});Interview tip: Explain that event listeners are built on top of callbacks but are designed specifically for the DOM event system. While callbacks are general-purpose, event listeners have special features like the event object, event delegation, and removal capability. Both achieve the same goal of 'deferred execution' but in different contexts.
Q7. How can you create a private counter using closures?
A private counter uses a closure to hide the counter variable from the global scope while providing methods to interact with it.
// Simple private counter
function createCounter() {
let count = 0; // Private variable
return {
increment: function() {
return ++count;
},
decrement: function() {
return --count;
},
getCount: function() {
return count;
}
};
}
const counter = createCounter();
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
console.log(counter.decrement()); // 1
console.log(counter.getCount()); // 1
// count is private
console.log(counter.count); // undefined
// Multiple independent counters
const counter1 = createCounter();
const counter2 = createCounter();
counter1.increment();
counter1.increment();
counter2.increment();
console.log(counter1.getCount()); // 2
console.log(counter2.getCount()); // 1 - independent counters!
// Advanced: Counter with step
function createStepCounter(step = 1) {
let count = 0;
return {
increment: () => (count += step),
decrement: () => (count -= step),
reset: () => (count = 0),
getCount: () => count,
setStep: (newStep) => (step = newStep)
};
}
const stepCounter = createStepCounter(5);
console.log(stepCounter.increment()); // 5
console.log(stepCounter.increment()); // 10
stepCounter.setStep(10);
console.log(stepCounter.increment()); // 20
// Counter with callback on change
function createObservableCounter(onChangeCallback) {
let count = 0;
return {
increment: () => {
count++;
onChangeCallback(count);
return count;
},
decrement: () => {
count--;
onChangeCallback(count);
return count;
},
getCount: () => count
};
}
const observableCounter = createObservableCounter(function(newCount) {
console.log(`Counter changed to: ${newCount}`);
});
observableCounter.increment(); // Counter changed to: 1
observableCounter.increment(); // Counter changed to: 2
observableCounter.decrement(); // Counter changed to: 1
// Using arrow functions
const arrowCounter = (() => {
let count = 0;
return {
inc: () => ++count,
dec: () => --count,
get: () => count
};
})();
console.log(arrowCounter.inc()); // 1
// Counter with min/max bounds
function createBoundedCounter(min = 0, max = 10) {
let count = min;
return {
increment: () => {
if (count < max) count++;
return count;
},
decrement: () => {
if (count > min) count--;
return count;
},
getCount: () => count,
getRange: () => ({ min, max })
};
}
const boundedCounter = createBoundedCounter(0, 5);
console.log(boundedCounter.increment()); // 1
console.log(boundedCounter.increment()); // 2
// ... increments up to 5
console.log(boundedCounter.increment()); // 5 (max reached)
console.log(boundedCounter.increment()); // 5 (stays at max)Interview tip: Emphasize that the counter variable is completely private - there's no way to access or modify it directly from outside the function. This is data encapsulation at its finest. Discuss how each call to createCounter() creates a new independent counter with its own private state. This is a practical and common use of closures.
Q8. What is the relationship between closures and memory leaks?
Closures can potentially cause memory leaks if they retain references to large objects that are no longer needed. Understanding this relationship is important for writing efficient code.
// Potential memory leak with closures
function createHeavyProcessor() {
// Large object that uses memory
const largeData = new Array(1000000).fill('data');
return function process() {
// This closure retains reference to largeData
// Even if we only use one property, the whole object stays in memory
console.log(largeData[0]);
};
}
const processor = createHeavyProcessor();
// largeData is retained in memory as long as processor exists
// SOLUTION 1: Extract only needed data
function createEfficientProcessor() {
const largeData = new Array(1000000).fill('data');
const firstElement = largeData[0]; // Extract what we need
// Now we can let largeData be garbage collected
return function process() {
console.log(firstElement); // Only retain what's needed
};
}
// Event listener memory leak
const button = document.querySelector('button');
let count = 0;
function setupEventListener() {
const largeObject = { data: new Array(1000000) };
button.addEventListener('click', function() {
count++;
console.log(largeObject.data[0]); // Retains largeObject
});
}
setupEventListener();
// largeObject stays in memory even if button is removed from DOM
// SOLUTION: Remove listener when done
function setupEventListenerProperly() {
const largeObject = { data: new Array(1000000) };
const clickHandler = function() {
count++;
// Use only what's needed
};
button.addEventListener('click', clickHandler);
// Later, when done:
// button.removeEventListener('click', clickHandler);
}
// Closure reference cycle
function createNode(value) {
const data = { value };
return {
getData: () => data,
setData: (newData) => { data.value = newData; },
// This creates circular reference
getThis: function() { return this; } // 'this' refers back to parent object
};
}
const node = createNode(42);
// node -> closure scope -> data
// node -> getThis() -> node (circular!)
// SOLUTION: Break circular references when done
node = null; // Allows garbage collection
// DOM element closure leak
function setupDOMListener() {
const element = document.querySelector('.element');
button.addEventListener('click', function() {
console.log(element.textContent);
});
}
setupDOMListener();
// Even if element is removed from DOM, it's retained by event listener closure
// SOLUTION: Clean up properly
function setupDOMListenerProperly() {
let element = document.querySelector('.element');
const handler = function() {
if (element) {
console.log(element.textContent);
}
};
button.addEventListener('click', handler);
// Later, cleanup:
function cleanup() {
button.removeEventListener('click', handler);
element = null; // Release reference
}
return cleanup;
}
const cleanup = setupDOMListenerProperly();
// cleanup() when done
// Timers with closures
function setupTimer() {
const largeData = new Array(1000000);
setInterval(function() {
console.log(largeData[0]);
}, 1000);
// largeData is retained in memory indefinitely!
}
// SOLUTION: Clear timer and nullify reference
function setupTimerProperly() {
let largeData = new Array(1000000);
const timerId = setInterval(function() {
if (largeData) {
console.log(largeData[0]);
}
}, 1000);
function cleanup() {
clearInterval(timerId);
largeData = null; // Release reference
}
return cleanup;
}
const timerCleanup = setupTimerProperly();
// timerCleanup() when doneInterview tip: Explain that closures themselves aren't bad - they're a powerful feature. The issue arises when closures retain references to large objects that are no longer needed. Discuss specific scenarios: event listeners not being removed, timers not being cleared, DOM elements held in memory. Mention tools like Chrome DevTools to detect memory leaks and emphasize proper cleanup patterns.
Closures and Callbacks
Learn how closures retain lexical environments, scoping rules, lexical scope, and passing callback functions for async operations.
What is a closure in JavaScript?
A closure is a function that has access to variables from another function's scope. This is possible because f...
What is lexical scoping and how does it relate to closures?
Lexical scoping means that the accessibility of variables is determined by the position of the variables in th...
What is a callback function and what are common use cases?
A callback function is a function that is passed as an argument to another function, and the receiving functio...
What is callback hell and how can you avoid it?
Callback hell (also called pyramid of doom) occurs when multiple nested callbacks are used, making the code ha...
How do closures relate to the module pattern?
The module pattern uses closures to create private and public members. It's a way to encapsulate code and avoi...
What is the difference between callback and event listener?
Both callbacks and event listeners use similar mechanisms, but they have different contexts and purposes. An e...
How can you create a private counter using closures?
A private counter uses a closure to hide the counter variable from the global scope while providing methods to...
What is the relationship between closures and memory leaks?
Closures can potentially cause memory leaks if they retain references to large objects that are no longer need...