JavaScript Closures Interview Questions & Answers
Closures are one of the most powerful and fundamental concepts in JavaScript. They allow functions to “remember” and access variables from their outer scope even after the outer function has finished executing.
Table of Contents
- Closure Basics
- Scope and Lexical Environment
- Practical Applications
- Memory Management
- Common Patterns
- Advanced Closure Concepts
Closure Basics
1. What is a closure in JavaScript?
A closure is a function that has access to variables in its outer (enclosing) scope even after the outer function has returned. The closure “remembers” the environment in which it was created.
Basic example:
function outerFunction(x) {
return function innerFunction(y) {
return x + y; // innerFunction has access to x from outerFunction
};
}
const addFive = outerFunction(5);
console.log(addFive(3)); // 8
console.log(addFive(10)); // 15
Key characteristics:
- Functions can access variables from their outer scope
- The outer function’s variables remain in memory
- Each closure maintains its own copy of the outer variables
- Closures are created at function creation time, not execution time
2. How do closures work with scope?
Closures work through JavaScript’s lexical scoping (also called static scoping), where the scope is determined by the location of the function declaration in the source code.
let globalVar = "I'm global";
function outer() {
let outerVar = "I'm from outer";
function inner() {
let innerVar = "I'm from inner";
function deepest() {
console.log(globalVar); // Access global
console.log(outerVar); // Access outer scope
console.log(innerVar); // Access inner scope
}
return deepest;
}
return inner();
}
const closure = outer();
closure(); // All variables are accessible
Scope chain example:
function createCounter() {
let count = 0; // Private variable
return {
increment: function() {
count++;
return count;
},
decrement: function() {
count--;
return count;
},
getCount: function() {
return count;
}
};
}
const counter = createCounter();
console.log(counter.getCount()); // 0
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
console.log(counter.decrement()); // 1
// count variable is not accessible from outside
3. What is the difference between closure and scope?
| Scope | Closure |
|---|---|
| Determines variable accessibility | Function that remembers its outer scope |
| Static (lexical) or dynamic | Always lexical (static) |
| Exists at function creation | Created when function is defined |
| Can be global, function, or block | Always involves at least two scopes |
Example demonstrating the difference:
// Scope example
function scopeExample() {
let x = 10;
if (true) {
let y = 20;
console.log(x); // 10 (accessing outer scope)
console.log(y); // 20 (accessing current scope)
}
// console.log(y); // ReferenceError: y is not defined
}
// Closure example
function closureExample() {
let x = 10;
return function() {
return x; // This function "closes over" x
};
}
const getX = closureExample();
console.log(getX()); // 10 (x is still accessible)
Scope and Lexical Environment
4. What is lexical scoping and how does it relate to closures?
Lexical scoping means that the scope of a variable is determined by its position within the source code, and nested functions have access to variables declared in their outer scope.
function outer() {
let message = "Hello from outer";
function inner() {
let innerMessage = "Hello from inner";
function deepest() {
console.log(message); // Access outer scope
console.log(innerMessage); // Access inner scope
}
return deepest;
}
return inner();
}
const closure = outer();
closure(); // Both messages are accessible
Lexical environment example:
function createGreeter(greeting) {
return function(name) {
return `${greeting}, ${name}!`;
};
}
const sayHello = createGreeter("Hello");
const sayGoodbye = createGreeter("Goodbye");
console.log(sayHello("John")); // "Hello, John!"
console.log(sayGoodbye("Jane")); // "Goodbye, Jane!"
5. How do closures handle variable references vs values?
Closures capture references to variables, not their values at the time of creation. This can lead to unexpected behavior in loops.
Common mistake (capturing reference):
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]()); // 3 (not 0!)
console.log(funcs[1]()); // 3 (not 1!)
console.log(funcs[2]()); // 3 (not 2!)
Solutions:
Solution 1: Using let (block scope)
function createFunctions() {
const functions = [];
for (let i = 0; i < 3; i++) {
functions.push(function() {
return i;
});
}
return functions;
}
const funcs = createFunctions();
console.log(funcs[0]()); // 0
console.log(funcs[1]()); // 1
console.log(funcs[2]()); // 2
Solution 2: Using IIFE (Immediately Invoked Function Expression)
function createFunctions() {
const functions = [];
for (var i = 0; i < 3; i++) {
functions.push((function(index) {
return function() {
return index;
};
})(i));
}
return functions;
}
const funcs = createFunctions();
console.log(funcs[0]()); // 0
console.log(funcs[1]()); // 1
console.log(funcs[2]()); // 2
Solution 3: Using bind
function createFunctions() {
const functions = [];
for (var i = 0; i < 3; i++) {
functions.push(function(index) {
return index;
}.bind(null, i));
}
return functions;
}
const funcs = createFunctions();
console.log(funcs[0]()); // 0
console.log(funcs[1]()); // 1
console.log(funcs[2]()); // 2
Practical Applications
6. What are common use cases for closures?
1. Data Privacy (Module Pattern)
function createCounter() {
let count = 0; // Private variable
return {
increment: () => ++count,
decrement: () => --count,
getCount: () => count,
reset: () => count = 0
};
}
const counter = createCounter();
console.log(counter.getCount()); // 0
counter.increment();
counter.increment();
console.log(counter.getCount()); // 2
// count is not accessible from outside
2. Function Factories
function multiply(x) {
return function(y) {
return x * y;
};
}
const multiplyByTwo = multiply(2);
const multiplyByTen = multiply(10);
console.log(multiplyByTwo(5)); // 10
console.log(multiplyByTen(5)); // 50
3. Partial Application/Currying
function add(a, b, c) {
return a + b + c;
}
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
}
return function(...moreArgs) {
return curried.apply(this, args.concat(moreArgs));
};
};
}
const curriedAdd = curry(add);
const addFive = curriedAdd(5);
const addFiveAndThree = addFive(3);
console.log(addFiveAndThree(2)); // 10
4. Event Handlers
function createButtonHandler(buttonId) {
return function() {
console.log(`Button ${buttonId} was clicked!`);
};
}
// Simulating button creation
const button1Handler = createButtonHandler("btn1");
const button2Handler = createButtonHandler("btn2");
button1Handler(); // "Button btn1 was clicked!"
button2Handler(); // "Button btn2 was clicked!"
5. Memoization
function memoize(fn) {
const cache = {};
return function(...args) {
const key = JSON.stringify(args);
if (cache[key]) {
console.log("Returning cached result");
return cache[key];
}
console.log("Computing new result");
const result = fn.apply(this, args);
cache[key] = result;
return result;
};
}
const expensiveFunction = memoize(function(n) {
return n * n;
});
console.log(expensiveFunction(5)); // "Computing new result" -> 25
console.log(expensiveFunction(5)); // "Returning cached result" -> 25
7. How do you create private variables using closures?
Closures provide a way to create truly private variables in JavaScript.
Basic private variable example:
function createPerson(name) {
let age = 0; // Private variable
return {
getName: function() {
return name;
},
getAge: function() {
return age;
},
setAge: function(newAge) {
if (newAge >= 0) {
age = newAge;
}
},
haveBirthday: function() {
age++;
}
};
}
const person = createPerson("John");
console.log(person.getName()); // "John"
console.log(person.getAge()); // 0
person.setAge(25);
console.log(person.getAge()); // 25
person.haveBirthday();
console.log(person.getAge()); // 26
// name and age are not directly accessible
Module pattern with private methods:
const calculator = (function() {
// Private variables
let result = 0;
// Private methods
function validateNumber(num) {
return typeof num === 'number' && !isNaN(num);
}
function logOperation(operation, num) {
console.log(`Performing ${operation} with ${num}`);
}
// Public API
return {
add: function(num) {
if (validateNumber(num)) {
logOperation('addition', num);
result += num;
}
return this;
},
subtract: function(num) {
if (validateNumber(num)) {
logOperation('subtraction', num);
result -= num;
}
return this;
},
multiply: function(num) {
if (validateNumber(num)) {
logOperation('multiplication', num);
result *= num;
}
return this;
},
getResult: function() {
return result;
},
reset: function() {
result = 0;
return this;
}
};
})();
console.log(calculator.add(5).multiply(2).subtract(3).getResult()); // 7
Memory Management
8. How do closures affect memory management?
Closures can lead to memory leaks if not handled properly, as they keep references to their outer scope variables.
Potential memory leak example:
function createHeavyObject() {
const heavyData = new Array(1000000).fill('data'); // Large array
return function() {
console.log(heavyData.length); // This keeps heavyData in memory
};
}
const heavyFunction = createHeavyObject();
// heavyData remains in memory as long as heavyFunction exists
Memory leak prevention:
function createHeavyObject() {
const heavyData = new Array(1000000).fill('data');
return function() {
console.log(heavyData.length);
// Clear reference when done
heavyData.length = 0;
};
}
// Or better: don't capture heavy data if not needed
function createLightweightFunction() {
return function() {
console.log("Lightweight operation");
};
}
Garbage collection and closures:
function outer() {
let largeData = new Array(1000000).fill('data');
return function inner() {
// Only use what you need
console.log("Processing...");
// largeData is kept in memory even if not used
};
}
// To allow garbage collection:
function outer() {
let largeData = new Array(1000000).fill('data');
const inner = function() {
console.log("Processing...");
};
// Clear reference when outer function ends
largeData = null;
return inner;
}
9. How do you avoid memory leaks with closures?
Best practices for memory management:
1. Clear references when done:
function createEventListener() {
const element = document.getElementById('myButton');
const handler = function() {
console.log('Button clicked');
};
element.addEventListener('click', handler);
// Return cleanup function
return function cleanup() {
element.removeEventListener('click', handler);
// Clear references
element = null;
};
}
const cleanup = createEventListener();
// Call cleanup when done
cleanup();
2. Use WeakMap/WeakSet for object references:
const cache = new WeakMap();
function expensiveOperation(obj) {
if (cache.has(obj)) {
return cache.get(obj);
}
const result = /* expensive computation */;
cache.set(obj, result);
return result;
}
3. Avoid capturing large objects unnecessarily:
// Bad: Captures entire large object
function badClosure(largeObject) {
return function() {
console.log(largeObject.someProperty);
};
}
// Good: Only capture what you need
function goodClosure(largeObject) {
const neededProperty = largeObject.someProperty;
return function() {
console.log(neededProperty);
};
}
Common Patterns
10. What is the module pattern and how do closures enable it?
The module pattern uses closures to create private and public APIs, providing encapsulation and avoiding global namespace pollution.
Basic module pattern:
const myModule = (function() {
// Private variables
let privateVar = "I'm private";
// Private function
function privateFunction() {
return "This is private";
}
// Public API
return {
publicVar: "I'm public",
publicFunction: function() {
return privateFunction() + " but accessible publicly";
},
getPrivateVar: function() {
return privateVar;
},
setPrivateVar: function(value) {
privateVar = value;
}
};
})();
console.log(myModule.publicVar); // "I'm public"
console.log(myModule.publicFunction()); // "This is private but accessible publicly"
console.log(myModule.getPrivateVar()); // "I'm private"
myModule.setPrivateVar("Updated private");
console.log(myModule.getPrivateVar()); // "Updated private"
// privateVar and privateFunction are not accessible directly
Revealing module pattern:
const calculator = (function() {
// Private variables
let result = 0;
// Private functions
function add(a, b) {
return a + b;
}
function subtract(a, b) {
return a - b;
}
function multiply(a, b) {
return a * b;
}
// Public API (reveals private functions)
return {
add: add,
subtract: subtract,
multiply: multiply,
calculate: function(operation, a, b) {
switch(operation) {
case 'add': return add(a, b);
case 'subtract': return subtract(a, b);
case 'multiply': return multiply(a, b);
default: throw new Error('Unknown operation');
}
}
};
})();
console.log(calculator.calculate('add', 5, 3)); // 8
console.log(calculator.calculate('multiply', 4, 2)); // 8
11. What is currying and how do closures implement it?
Currying is a technique of evaluating a function with multiple arguments by converting it into a sequence of functions with a single argument.
Basic currying:
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
}
return function(...moreArgs) {
return curried.apply(this, args.concat(moreArgs));
};
};
}
// Example usage
function add(a, b, c) {
return a + b + c;
}
const curriedAdd = curry(add);
console.log(curriedAdd(1)(2)(3)); // 6
console.log(curriedAdd(1, 2)(3)); // 6
console.log(curriedAdd(1)(2, 3)); // 6
Practical currying example:
function createLogger(level) {
return function(message) {
return function(timestamp) {
return `[${timestamp}] ${level.toUpperCase()}: ${message}`;
};
};
}
const infoLogger = createLogger('info')('User logged in');
const errorLogger = createLogger('error')('Database connection failed');
console.log(infoLogger(new Date().toISOString()));
console.log(errorLogger(new Date().toISOString()));
12. What is partial application and how does it differ from currying?
Partial application is a technique where you create a new function by fixing some of the arguments of an existing function.
Partial application vs currying:
// Currying: transforms function to accept one argument at a time
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
}
return function(...moreArgs) {
return curried.apply(this, args.concat(moreArgs));
};
};
}
// Partial application: fixes some arguments
function partial(fn, ...fixedArgs) {
return function(...remainingArgs) {
return fn.apply(this, fixedArgs.concat(remainingArgs));
};
}
// Example
function greet(greeting, name, punctuation) {
return `${greeting}, ${name}${punctuation}`;
}
// Currying
const curriedGreet = curry(greet);
const greetHello = curriedGreet("Hello");
const greetHelloJohn = greetHello("John");
console.log(greetHelloJohn("!")); // "Hello, John!"
// Partial application
const greetHelloPartial = partial(greet, "Hello");
console.log(greetHelloPartial("John", "!")); // "Hello, John!"
Advanced Closure Concepts
13. How do closures work with async operations?
Closures are particularly useful with async operations as they can capture and maintain state across asynchronous calls.
Closure with setTimeout:
function createDelayedLogger(message, delay) {
return function() {
setTimeout(function() {
console.log(message);
}, delay);
};
}
const logAfter1Second = createDelayedLogger("Hello after 1 second", 1000);
const logAfter2Seconds = createDelayedLogger("Hello after 2 seconds", 2000);
logAfter1Second();
logAfter2Seconds();
Closure with Promise:
function createAsyncCounter() {
let count = 0;
return {
increment: function() {
return new Promise((resolve) => {
setTimeout(() => {
count++;
resolve(count);
}, 1000);
});
},
getCount: function() {
return Promise.resolve(count);
}
};
}
const counter = createAsyncCounter();
counter.increment().then(count => console.log(count)); // 1
counter.increment().then(count => console.log(count)); // 2
Closure with async/await:
function createDataProcessor() {
let processedData = [];
return {
async processData(data) {
// Simulate async processing
await new Promise(resolve => setTimeout(resolve, 100));
const processed = data.map(item => item * 2);
processedData.push(...processed);
return processed;
},
async getProcessedData() {
return processedData;
},
clearData() {
processedData = [];
}
};
}
const processor = createDataProcessor();
async function example() {
await processor.processData([1, 2, 3]);
await processor.processData([4, 5, 6]);
const allData = await processor.getProcessedData();
console.log(allData); // [2, 4, 6, 8, 10, 12]
}
14. How do closures work with event listeners and DOM manipulation?
Closures are essential for event handling as they allow you to maintain state and access variables across event callbacks.
Basic event listener with closure:
function createButtonCounter() {
let count = 0;
return function() {
count++;
console.log(`Button clicked ${count} times`);
return count;
};
}
// Simulating button event listener
const buttonHandler = createButtonCounter();
buttonHandler(); // "Button clicked 1 times"
buttonHandler(); // "Button clicked 2 times"
buttonHandler(); // "Button clicked 3 times"
DOM manipulation with closure:
function createToggleButton(buttonId, textOn, textOff) {
let isOn = false;
return function() {
isOn = !isOn;
const button = document.getElementById(buttonId);
if (button) {
button.textContent = isOn ? textOn : textOff;
button.className = isOn ? 'active' : 'inactive';
}
return isOn;
};
}
// Usage (in browser environment)
// const toggleHandler = createToggleButton('myButton', 'ON', 'OFF');
// document.getElementById('myButton').addEventListener('click', toggleHandler);
Multiple event listeners with shared state:
function createFormValidator() {
let errors = [];
let isValid = true;
return {
addError: function(field, message) {
errors.push({ field, message });
isValid = false;
},
clearErrors: function() {
errors = [];
isValid = true;
},
getErrors: function() {
return [...errors];
},
validate: function() {
return isValid;
}
};
}
const validator = createFormValidator();
validator.addError('email', 'Invalid email format');
validator.addError('password', 'Password too short');
console.log(validator.getErrors()); // Array of errors
console.log(validator.validate()); // false
15. How do closures work with generators and iterators?
Closures can be used with generators to maintain state across multiple yield operations.
Generator with closure:
function createCounter() {
let count = 0;
return function* counterGenerator() {
while (true) {
yield count++;
}
};
}
const counterGen = createCounter();
const iterator = counterGen();
console.log(iterator.next().value); // 0
console.log(iterator.next().value); // 1
console.log(iterator.next().value); // 2
Closure with custom iterator:
function createRangeIterator(start, end, step = 1) {
let current = start;
return {
next: function() {
if (current <= end) {
const value = current;
current += step;
return { value, done: false };
}
return { done: true };
}
};
}
const range = createRangeIterator(1, 5, 2);
console.log(range.next()); // { value: 1, done: false }
console.log(range.next()); // { value: 3, done: false }
console.log(range.next()); // { value: 5, done: false }
console.log(range.next()); // { done: true }
16. How do closures work with decorators and higher-order functions?
Closures are fundamental to implementing decorators and higher-order functions.
Function decorator with closure:
function withLogging(fn) {
return function(...args) {
console.log(`Calling ${fn.name} with arguments:`, args);
const result = fn.apply(this, args);
console.log(`${fn.name} returned:`, result);
return result;
};
}
function add(a, b) {
return a + b;
}
const loggedAdd = withLogging(add);
console.log(loggedAdd(2, 3)); // Logs the call and result, then returns 5
Performance decorator:
function measureTime(fn) {
return function(...args) {
const start = performance.now();
const result = fn.apply(this, args);
const end = performance.now();
console.log(`${fn.name} took ${end - start} milliseconds`);
return result;
};
}
function slowFunction() {
// Simulate slow operation
for (let i = 0; i < 1000000; i++) {
Math.random();
}
return "Done";
}
const measuredSlowFunction = measureTime(slowFunction);
measuredSlowFunction(); // Logs execution time
Caching decorator:
function memoize(fn) {
const cache = new Map();
return function(...args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
console.log('Returning cached result');
return cache.get(key);
}
console.log('Computing new result');
const result = fn.apply(this, args);
cache.set(key, result);
return result;
};
}
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
const memoizedFibonacci = memoize(fibonacci);
console.log(memoizedFibonacci(10)); // Computes and caches
console.log(memoizedFibonacci(10)); // Returns cached result
Practice Questions
17. What will be the output of this closure code?
function outer() {
let x = 10;
function inner() {
let y = 20;
function deepest() {
console.log(x + y);
}
return deepest;
}
return inner();
}
const closure = outer();
closure(); // What will this output?
30
Explanation: The deepest function has access to both x (from outer) and y (from inner) through closure. When closure() is called, it executes deepest() which logs x + y = 10 + 20 = 30.
18. What will be the output of this loop with closures?
function createFunctions() {
const functions = [];
for (let i = 0; i < 3; i++) {
functions.push(function() {
return i;
});
}
return functions;
}
const funcs = createFunctions();
console.log(funcs[0]()); // What will this output?
console.log(funcs[1]()); // What will this output?
console.log(funcs[2]()); // What will this output?
0, 1, 2
Explanation: Using let creates block scope for each iteration, so each function captures its own copy of i at the time of creation.
19. How would you fix this memory leak?
function createEventListener() {
const element = document.getElementById('myButton');
const handler = function() {
console.log('Button clicked');
};
element.addEventListener('click', handler);
// This creates a memory leak - how to fix it?
}
Return a cleanup function:
function createEventListener() {
const element = document.getElementById('myButton');
const handler = function() {
console.log('Button clicked');
};
element.addEventListener('click', handler);
// Return cleanup function
return function cleanup() {
element.removeEventListener('click', handler);
// Clear references
element = null;
};
}
const cleanup = createEventListener();
// Call cleanup when done
cleanup();
20. Create a closure that implements a private counter with increment, decrement, and reset methods.
Solution:
function createCounter(initialValue = 0) {
let count = initialValue;
return {
increment: function() {
count++;
return count;
},
decrement: function() {
count--;
return count;
},
reset: function() {
count = initialValue;
return count;
},
getValue: function() {
return count;
}
};
}
const counter = createCounter(5);
console.log(counter.getValue()); // 5
console.log(counter.increment()); // 6
console.log(counter.increment()); // 7
console.log(counter.decrement()); // 6
console.log(counter.reset()); // 5
Summary
Closures are a fundamental concept in JavaScript that enable:
- Data privacy through encapsulation
- State management across function calls
- Function factories and partial application
- Module patterns for code organization
- Event handling with persistent state
- Memoization and performance optimization
Key points to remember:
- Closures capture references, not values
- They can lead to memory leaks if not managed properly
- They’re essential for modern JavaScript patterns
- They work with all JavaScript features (async, generators, etc.)
- They provide true privacy in JavaScript
Understanding closures is crucial for writing maintainable, efficient, and well-structured JavaScript code.
Interview angle
- “What is a closure?” - a function together with the scope it was created in, keeping those variables alive after the outer function returns. It is the mechanism behind private state, memoization, currying and every callback that remembers something.
- “Why does the classic
varloop print the final value N times?” -varis function-scoped, so all callbacks close over one binding.letis block-scoped and creates a fresh binding per iteration, which is why the same loop works withlet. - “How do closures leak memory?” - by capturing more than they need. A closure holding a reference to a large object or a DOM node keeps it alive; long-lived handlers and unremoved event listeners are the usual culprits.
- “Where do closures bite in React?” - stale closures. An effect or callback captures the props and state of the render it was created in, so a value read inside a
setIntervalstays frozen unless the dependency array or a ref keeps it current. See ../05_react/stale_closures_and_hook_rules.md.