Closures and currying — common interview combinations
Closures: inner functions remember their enclosing scope. Currying turns `f(a,b,c)` into `f(a)(b)(c)` using nested closures. Combined patterns interviewers love: counter factories, once/memoize, partial application, debounce/throttle, infinitely-curryable functions like `sum(1)(2)(3)... .valueOf()`.
Closures + currying is one of the most common interview pairings. Both rely on the same mechanism: a function remembering variables from the scope it was created in.
Closure refresher
function makeCounter() {
let count = 0;
return () => ++count;
}
const c = makeCounter();
c(); c(); c(); // 1, 2, 3count survives because the returned function holds a reference to its scope.
Currying — function of N args → chain of unary functions
const curry = (fn) => {
return function curried(...args) {
if (args.length >= fn.length) return fn.apply(this, args);
return (...more) => curried.apply(this, [...args, ...more]);
};
};
const add = (a, b, c) => a + b + c;
const cAdd = curry(add);
cAdd(1)(2)(3); // 6
cAdd(1, 2)(3); // 6
cAdd(1)(2, 3); // 6Patterns interviewers ask
1. Counter factory (closures)
function makeCounter(start = 0) {
return {
inc: () => ++start,
dec: () => --start,
value: () => start,
};
}2. once (closure)
function once(fn) {
let called = false, result;
return (...args) => (called ? result : (called = true, result = fn(...args)));
}3. memoize (closure)
function memoize(fn) {
const cache = new Map();
return (...args) => {
const key = JSON.stringify(args);
if (!cache.has(key)) cache.set(key, fn(...args));
return cache.get(key);
};
}4. Partial application (currying lite)
const partial = (fn, ...preset) => (...rest) => fn(...preset, ...rest);
const greet = (greeting, name) => `${greeting}, ${name}!`;
const hi = partial(greet, "Hi");
hi("Anya"); // "Hi, Anya!"5. Infinitely curryable sum
function sum(a) {
let total = a;
const inner = (b) => { total += b; return inner; };
inner.valueOf = () => total;
return inner;
}
sum(1)(2)(3) + 0; // 6 — coerces via valueOf6. Debounce/throttle (closure-based)
The lastCall, timer, and lastArgs live in the closure (see [[how-to-implement-throttling]]).
7. Module pattern (pre-ES6)
const counter = (function() {
let n = 0;
return { inc: () => ++n, get: () => n };
})();Closure gotchas
Loop variable capture (var)
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0); // 3, 3, 3 — shared var
}Fix: let (block scope), or IIFE wrapper for the var case.
Memory leaks via captured DOM
A closure that captures a large object (or DOM node) keeps it alive — be intentional about what you close over.
Interview framing
"Closures let inner functions remember the scope they were created in — that's the foundation of factories, once, memoize, debouncing, and the module pattern. Currying applies that to transform f(a,b,c) into a chain of unary calls — useful for partial application and point-free composition. The classic combinations interviewers love are: a counter factory, once/memoize, an infinitely-curryable sum via valueOf, partial application, and the var-in-loop closure puzzle. The underlying mechanism is the same in all of them — a function holding a reference to its lexical environment."
Follow-up questions
- •Implement curry that supports placeholders (curry(add)(_, 2)(1)).
- •Why does the var-in-loop closure print 3,3,3?
- •Implement memoize with a custom cache key.
- •How does the infinitely-curryable sum trick work?
Common mistakes
- •Using JSON.stringify for memoize keys with non-serializable args (functions, circular refs).
- •Forgetting that var is function-scoped — closure captures the same binding.
- •Currying without supporting variadic last-call (a, b, c) | (a)(b)(c) | (a, b)(c).
- •Memory leaks by closing over large objects you no longer need.
Performance considerations
- •Closures are cheap; the cost is in what they capture. Memoize speeds repeat calls at the cost of memory; cap the cache for hot paths.
Edge cases
- •Currying a function with default parameters — fn.length excludes them.
- •Currying a function that uses this — bind appropriately.
- •Memoize cache growing unbounded — consider LRU.
Real-world examples
- •Lodash curry/partial/once/memoize.
- •React hooks themselves are closure-based (each render closes over its values).