JavaScript closures interview questions, with answers
Every JavaScript interview reaches closures eventually, because they test whether you understand functions as values that carry their surroundings with them. The definition fits in a sentence; the questions are about its consequences — why functions created in a loop can share one variable, how a closure keeps data private, what memoize, debounce and curry have in common, and when a closure keeps memory alive longer than intended. Every output shown was produced by running the code in Node.js.
The questions start with the definition and the loop bug, then the patterns closures make possible, then their cost. Then take the free JavaScript diagnostic — ten questions across every JavaScript topic in the bank — to see which of these you can explain but not yet predict.
The questions, with answers
1.What is a closure in JavaScript?
In short: A closure is a function together with the variables of the scope it was created in, which it can keep reading and updating after that scope has finished running.
Every JavaScript function remembers the scope it was defined in, its lexical environment. When a function is returned from another function, or stored somewhere, it keeps that environment alive, so the outer function's local variables survive the outer call and stay reachable through the inner function, as add5 does below. The closure holds variables, not copies of their values: if a variable changes, the function sees the change. Each call of the outer function creates a new environment, so functions produced by separate calls do not share state. Closures are how callbacks remember their context, how event handlers know which item they belong to, and how private state works without classes.
function adder(n) { return (x) => x + n; } const add5 = adder(5); console.log(add5(2)); // 7
2.Why do functions created in a loop with var all see the same value?
In short: A var loop variable is one binding for the whole loop, so every function created inside closes over the same variable and sees its final value; let creates a new binding for each iteration.
The functions do not capture the value of i at the moment they are created; they capture the variable. With var there is only one i, scoped to the function, and by the time any of the functions runs, the loop has finished and i holds the value that ended it, so the first function below computes 3 * 10, not 1 * 10. The same thing happens with callbacks that run later, such as timers and event handlers, which is where the bug usually appears. let fixes it because the language gives a let loop variable a fresh binding on each iteration, copied from the previous one, so each function closes over its own. Before ES2015 the fix was an IIFE that took i as a parameter.
const fs = []; for (var i = 1; i <= 2; i++) { fs.push(() => i * 10); } console.log(fs[0]()); // 30 const gs = []; for (let j = 1; j <= 2; j++) { gs.push(() => j * 10); } console.log(gs[0]()); // 10
3.How do closures give JavaScript private variables?
In short: Variables declared in an outer function are unreachable from outside once it returns, so only the functions it returns can read or change them — state behind a controlled interface.
Return an object whose methods close over local variables, and those variables become private state: callers can use the methods but cannot touch the data, which is this page's sample. This is the module pattern, and it gave JavaScript encapsulation long before classes existed. Classes now have true private fields, written with a # prefix and enforced by the language: writing obj.#field outside the class body is a SyntaxError. Closure-based privacy still suits factory functions and code that avoids this. The cost is memory: each object made by a factory carries its own copies of the method functions, where class methods live once on the prototype and are shared.
function account(pin) { let tries = 0; return { check(p) { tries++; return p === pin; }, attempts: () => tries, }; } const a = account(1234); a.check(1111); console.log(a.attempts()); // 1 console.log(a.pin); // undefined
4.How would you write a memoize function in JavaScript?
In short: Keep a cache in the outer function and return a wrapper that looks each argument up in it, calling the original function only on a miss and storing the result.
The cache must live between calls but stay out of reach, which is exactly what a closure provides. A Map keyed by the argument works for functions of one primitive argument, as below, where the second call is answered from the cache. For several arguments, the usual interview answer builds a key with JSON.stringify(args), which works for primitives but conflates values it cannot represent, such as undefined and functions. Memoizing only helps pure functions, whose result depends on nothing but their arguments, and the cache grows without limit unless you bound it, which is where an LRU cache comes in. For a recursive function such as Fibonacci, the recursive calls must go through the memoized version for the cache to help.
function memo(fn) { const cache = new Map(); return (n) => { if (!cache.has(n)) cache.set(n, fn(n)); return cache.get(n); }; } let calls = 0; const sq = memo((n) => { calls++; return n * n; }); sq(9); console.log(sq(9), calls); // 81 1
5.What are debounce and throttle, and how do closures implement them?
In short: Debounce runs a function only after calls have stopped for a set delay; throttle runs it at most once per interval — and both keep their timer or timestamp in a closure.
Both limit how often an expensive handler runs for rapid events such as typing, scrolling or resizing. A debounced search box waits until the user pauses, so it sends one request instead of one per keystroke: each call clears the pending timer and starts a new one, which is why only the last call below prints. A throttled scroll handler runs straight away and then ignores calls until the interval has passed, so it fires at a steady rate while events continue. The timer id, or the time of the last run, has to survive between calls without being global, which is the closure's job. Interview versions also pass along the latest call's arguments, as the rest parameter does below.
function debounce(fn, ms) { let t; return (...args) => { clearTimeout(t); t = setTimeout( () => fn(...args), ms); }; } const log = debounce( (q) => console.log(q), 50); log("j"); log("ja"); log("java"); // java
6.What is currying, and how is it related to closures?
In short: Currying turns a function of several arguments into a chain of functions that each take some of them, and every function in the chain is a closure over the arguments already supplied.
A curried volume(l)(w)(h) returns a function after each call until it has every argument, then computes the result. Each intermediate function remembers the arguments given so far through its closure, which makes partial application natural: fixing the first argument once gives a reusable function. A general curry helper, like the one below, compares the number of arguments collected with fn.length, the number of parameters fn declares, and either calls fn or returns a function that collects more. Interviewers use it to test whether you can reason about functions that return functions; in application code, bind and plain arrow functions are the more common ways to fix an argument.
function curry(fn) { return function next(...a) { if (a.length >= fn.length) return fn(...a); return (...b) => next(...a, ...b); }; } const vol = curry( (l, w, h) => l * w * h); console.log(vol(2)(3)(4)); // 24 console.log(vol(2, 3)(4)); // 24
7.How can closures cause memory leaks in JavaScript?
In short: A closure keeps its outer scope reachable for as long as the closure itself is reachable, so a long-lived callback can hold on to large data it no longer needs.
Garbage collection frees what cannot be reached, and a closure is a path to its scope. If an event listener, a timer callback or a cached function closes over a large array or a DOM node, that data lives as long as the listener or timer does. The common leaks are listeners that are never removed, intervals that are never cleared, and caches keyed by objects that should have died; a WeakMap fixes the last, because its keys do not keep objects alive. Engines such as V8 keep only the variables some closure actually uses, but closures created in one scope share them, so a variable used by any of them lives as long as the longest-lived one. Clearing the interval below lets its closure be collected.
let ticks = 0; const id = setInterval(() => { ticks++; if (ticks === 3) { clearInterval(id); console.log("stopped"); } }, 10); // stopped
How the diagnostic asks it
One question from the JavaScript bank, exactly as a sitting would show it. The bank has 3 on closures and 30 across JavaScript.
What does this code print?
const bank = (function () { let balance = 100; return { deposit: (x) => (balance += x) }; })(); bank.deposit(50); console.log(bank.balance, bank.deposit(0));
- 1150 150
- 2100 150
- 3TypeError is thrown
- 4undefined 150correct
balance is a local variable of the immediately invoked function; the returned object has only a deposit property, so bank.balance is undefined. deposit closes over balance and updates it, so after deposit(50) the balance is 150, and deposit(0) returns 150. 150 150 and 100 150 assume balance is a property of bank. Reading a missing property returns undefined rather than throwing, so there is no TypeError. This is the module pattern: closures give JavaScript private state.
Measure it
Reading answers tells you what’s true. A diagnostic tells you what you get wrong.
10 JavaScript questions across its topics, easy to hard, about fifteen minutes. You get a readiness figure with the arithmetic shown, the topics you missed named, and a practice set sized for today. Free: 1 diagnostic a month and 15 problems a day. No card.