Ten topics. Between them they produce more interview questions than anything else in JavaScript. Closures alone open perhaps a third of all screening rounds.
The good news: they are all the same idea seen from different angles. A function remembers where it was written. Get that, and the rest follows.
Lexical scope and the scope chain
In simple words: where a variable is available is decided by where you wrote the code, not by where you call it from. JavaScript decides this when it reads your file, before anything runs.
When a function needs a variable, it looks in its own body first. Not there? It looks at the function that wrapped it. Still not there? It keeps going outward until it reaches the global scope. That chain of lookups is the scope chain.
const app = "portfolio";
function outer() {
const user = "Subham";
function inner() {
console.log(user); // not local -> found in outer
console.log(app); // not in outer -> found in global
}
inner();
}Closures
In simple words: a closure is a function that still remembers the variables around it, even after the function that created those variables has finished.
That is the entire definition. The confusion comes from the fact that it sounds like it should be impossible.
function makeCounter() {
let count = 0; // outer finishes, but this survives
return function () {
count += 1;
return count;
};
}
const next = makeCounter();
next(); // 1
next(); // 2
next(); // 3
const other = makeCounter();
other(); // 1 -- a completely separate countmakeCounter returned and its call finished. Normally count would be discarded. But the returned function still refers to it, so JavaScript keeps it alive.
The loop and setTimeout puzzle
In simple words: the classic trick question. It is really a var versus let question wearing a costume.
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// 3 3 3
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// 0 1 2Why. var is function scoped, so there is exactly one i for the whole loop. All three callbacks close over that same variable. By the time the timers fire, the loop has finished and i is 3.
let is block scoped, and the for loop creates a fresh binding on every iteration. Three separate variables, three separate closures, three different values.
Three ways to fix the var version
// 1. Use let. Always the right answer in real code.
for (let i = 0; i < 3; i++) setTimeout(() => console.log(i), 0);
// 2. IIFE - create a new scope by hand, the pre-ES6 fix
for (var i = 0; i < 3; i++) {
(function (captured) {
setTimeout(() => console.log(captured), 0);
})(i);
}
// 3. Pass the value through setTimeout's extra arguments
for (var i = 0; i < 3; i++) {
setTimeout((captured) => console.log(captured), 0, i);
}this
In simple words: in a normal function, this is decided by how the function is called, not where it was written. Four rules, checked in order.
| # | Rule | How it looks | What this is |
|---|---|---|---|
| 1 | new binding | new Person() | the brand new object |
| 2 | Explicit binding | fn.call(obj), fn.apply(obj), fn.bind(obj) | whatever you passed |
| 3 | Implicit binding | obj.method() | the object left of the dot |
| 4 | Default binding | fn() | undefined in strict mode, globalThis otherwise |
Arrow functions sit outside this list entirely. An arrow function has no this of its own. It uses the this from wherever it was written, and nothing can change that — not call, not bind.
const user = {
name: "Subham",
greet() {
console.log(this.name);
},
};
user.greet(); // "Subham" -- rule 3, object left of the dot
const detached = user.greet;
detached(); // undefined -- rule 4, the dot is gone
setTimeout(user.greet, 0); // undefined -- same problem, passed as a plain fn
setTimeout(() => user.greet(), 0); // "Subham" -- the dot survivescall, apply and bind
In simple words: three ways to say "run this function, but with this set to something I choose".
call(thisArg, a, b)— runs immediately, arguments listedapply(thisArg, [a, b])— runs immediately, arguments in an arraybind(thisArg, a)— does not run, returns a new function withthislocked in
function introduce(greeting, punctuation) {
return `${greeting}, I am ${this.name}${punctuation}`;
}
const me = { name: "Subham" };
introduce.call(me, "Hello", "!"); // "Hello, I am Subham!"
introduce.apply(me, ["Hi", "."]); // "Hi, I am Subham."
const boundIntro = introduce.bind(me, "Hey");
boundIntro("!!"); // "Hey, I am Subham!!"Implement your own bind
A very common senior-round request. Three things must work: this binding, partial application, and behaving correctly when the bound function is called with new.
Function.prototype.myBind = function (thisArg, ...boundArgs) {
const originalFn = this;
if (typeof originalFn !== "function") {
throw new TypeError("Bind must be called on a function");
}
function bound(...callArgs) {
// when used with `new`, ignore thisArg and use the fresh object
const isNew = this instanceof bound;
return originalFn.apply(isNew ? this : thisArg, [...boundArgs, ...callArgs]);
}
bound.prototype = Object.create(originalFn.prototype || null);
return bound;
};Arrow functions vs regular functions
In simple words: arrows are not just shorter syntax. They are missing several things on purpose.
| Regular function | Arrow function | |
|---|---|---|
this | set at call time by the four rules | inherited from where it was written |
arguments object | yes | no, use rest parameters |
Usable with new | yes | no, throws TypeError |
Has a prototype property | yes | no |
| Can be a generator | yes | no |
| Hoisting | declarations hoist fully | only the variable hoists |
When you must not use an arrow:
// 1. As an object method that needs `this`
const counter = {
count: 0,
bad: () => { this.count++; }, // `this` is not counter
good() { this.count++; },
};
// 2. As a constructor
const Person = (name) => { this.name = name; };
new Person("Subham"); // TypeError: not a constructor
// 3. As a prototype method
Dog.prototype.speak = () => this.name; // `this` is not the instanceWhen an arrow is exactly right: any callback inside a method, where you want the surrounding this.
class Timer {
constructor() { this.seconds = 0; }
start() {
setInterval(() => { this.seconds++; }, 1000); // arrow keeps `this`
}
}Closures you actually write
In simple words: closures are not a puzzle-only feature. Almost every utility function you use is one.
Private state
function createAccount(initial) {
let balance = initial; // genuinely unreachable from outside
return {
deposit(amount) { balance += amount; return balance; },
getBalance() { return balance; },
};
}
const account = createAccount(100);
account.deposit(50); // 150
account.balance; // undefined -- there is no way inonce
function once(fn) {
let called = false;
let result;
return function (...args) {
if (called) return result;
called = true;
result = fn.apply(this, args);
return result;
};
}
const init = once(() => console.log("setting up"));
init(); // logs
init(); // silentmemoize
function memoize(fn, keyFrom = (...args) => JSON.stringify(args)) {
const cache = new Map();
return function (...args) {
const key = keyFrom(...args);
if (cache.has(key)) return cache.get(key);
const value = fn.apply(this, args);
cache.set(key, value);
return value;
};
}Currying and partial application
In simple words: currying turns a function that takes three arguments into a chain of functions that each take one. Partial application just pre-fills some arguments and waits for the rest.
// plain
const add = (a, b, c) => a + b + c;
add(1, 2, 3); // 6
// curried
const curriedAdd = (a) => (b) => (c) => a + b + c;
curriedAdd(1)(2)(3); // 6A generic curry
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
}
return (...rest) => curried.apply(this, [...args, ...rest]);
};
}
const sum = curry((a, b, c) => a + b + c);
sum(1)(2)(3); // 6
sum(1, 2)(3); // 6
sum(1)(2, 3); // 6fn.length is the number of declared parameters. The moment enough have arrived, it runs.
Infinite currying
A separate puzzle. sum(1)(2)(3)() — the empty call signals the end.
function sum(a) {
let total = a;
function next(b) {
if (b === undefined) return total;
total += b;
return next;
}
return next;
}
sum(1)(2)(3)(); // 6IIFE and the module pattern
In simple words: an IIFE is a function you define and call in the same breath. Before ES modules existed, it was the only way to get a private scope.
(function () {
// everything here is invisible to the outside
})();
const CounterModule = (function () {
let count = 0; // private
return {
increment() { return ++count; },
reset() { count = 0; },
};
})();You rarely write these now — every ES module already has its own scope. But you will see them constantly in older code and in bundled output, so recognise the shape.
Composition and pipe
In simple words: small functions combined into bigger ones. pipe reads left to right, compose reads right to left.
const pipe = (...fns) => (input) => fns.reduce((acc, fn) => fn(acc), input);
const compose = (...fns) => (input) => fns.reduceRight((acc, fn) => fn(acc), input);
const trim = (s) => s.trim();
const lower = (s) => s.toLowerCase();
const slugify = (s) => s.replace(/\s+/g, "-");
const toSlug = pipe(trim, lower, slugify);
toSlug(" Hello World "); // "hello-world"Ten questions
Answer each out loud before revealing.
- 01Explain a closure to someone who has never heard the word, in two sentences.
A function that remembers the variables that were around it when it was created, even after the code that created them has finished running. Each time you run the outer function you get a fresh, independent set of those variables.
If you started with the word "scope" or "execution context", try again — the interviewer is testing whether you can explain it, not recite it.
- 02Why does a
varloop withsetTimeoutprint 3 3 3, and what are three ways to fix it?varis function scoped, so there is one sharedi. All three callbacks close over that same variable, and by the time the timers run the loop has ended withiat 3.Fixes: use
let, which creates a fresh binding each iteration; wrap the body in an IIFE that takesias a parameter; or passias a third argument tosetTimeout. - 03What is
thisinside a method that has been passed as a callback, and how do you fix it?undefinedin strict mode, or the global object otherwise. Passingobj.methodas a value strips the object before the dot, so the implicit binding is gone and the default rule applies.Fix by wrapping in an arrow —
() => obj.method()— or by binding once and storing the result. - 04Name the four rules for
thisin priority order, and say where arrow functions fit.1.
newbinding. 2. Explicit binding with call, apply or bind. 3. Implicit binding from the object before the dot. 4. Default binding, undefined in strict mode.Arrow functions do not participate. They have no
thisof their own and take it lexically from the enclosing scope, which is whycallandbindcannot change it. - 05Name four concrete differences between an arrow function and a regular function.
Arrows take
thislexically instead of at call time. They have noargumentsobject. They cannot be used withnew. They have noprototypeproperty, and they cannot be generators.Follow-up you should expect: when must you NOT use one? As an object method or prototype method that needs
this, and as a constructor. - 06Why does this fail to remove the listener?
el.removeEventListener("click", this.handler.bind(this))bindreturns a brand new function every time it is called. The function passed toremoveEventListeneris a different object from the one that was added, so nothing matches and nothing is removed.Bind once, store the result on the instance, then use that same reference for both add and remove.
- 07What is the memory cost of a closure, and when does it become a real leak?
A closure keeps its captured variables alive for as long as the closure itself is reachable, so they cannot be garbage collected.
It becomes a leak when the closure outlives its usefulness — an event listener that is never removed, an interval that is never cleared, or an unbounded memoisation cache in a long-running Node process.
- 08How does a generic
curryknow when to stop collecting arguments and actually run?It compares the number of collected arguments against
fn.length, which is the count of declared parameters. Once enough have arrived it applies the original function; otherwise it returns another collector.Worth noting the limitation:
fn.lengthignores rest parameters and parameters with defaults, so currying does not work on those. - 09How would you give an object genuinely private state, without
#fields?Declare the variable inside a factory function and return only the methods that need it. The variable is in the closure scope, so there is no property to read, no key to enumerate, and no way in from outside.
This is the module pattern, and it was the standard approach before private class fields existed.
- 10What does
once(fn)need to remember, and why must it store the result rather than just a flag?It needs a boolean for whether it has run, and the return value from that first run.
Storing only the flag would make every call after the first return
undefined, which is almost never what the caller wants. The point is that repeat calls behave as if they had run and got the same answer.
Six exercises
- 01
Implement Function.prototype.myBind
Support
thisbinding and partial application. Then make it behave correctly when the bound function is called withnew— the fresh object should win over the boundthis. Test both paths. - 02
Write memoize with a size cap
Start with the plain version, then add a maximum entry count that evicts the least recently used key. You have just built a small LRU cache, which is a separate interview question in its own right.
- 03
Write a generic curry
It should accept arguments one at a time, several at a time, or all at once. Then explain in one sentence why it cannot work on a function that uses rest parameters.
- 04
Fix the loop three ways
Take the
varplussetTimeoutloop and fix it withlet, with an IIFE, and with setTimeout's extra arguments. Then replace the timers with an array of functions called later, and confirm you get the same results. - 05
Build a rate-limited function
Write
limitCalls(fn, n)that lets a function run at most n times, then returns the last result forever after. Reuse what you learnt fromonce. - 06
Write pipe and compose
Implement both, then use
pipeto build a slug generator from four small string functions. Explain which reads more naturally to you and why.