JavaScript Closures Interview Questions & Answers

4 interview angles 16 min read source

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

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 var loop print the final value N times?” - var is function-scoped, so all callbacks close over one binding. let is block-scoped and creates a fresh binding per iteration, which is why the same loop works with let.
  • “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 setInterval stays frozen unless the dependency array or a ref keeps it current. See ../05_react/stale_closures_and_hook_rules.md.