Closures
Reason precisely about retained lexical scope, callbacks, and encapsulation.

WHAT YOU WILL BE ABLE TO DO
Learning outcomes
- Explain closures in plain language.
- Connect the behavior to the underlying browser or framework model.
- Implement the core pattern and reason through edge cases.
- Answer common follow-ups without relying on memorized phrases.
01What it is
A closure is the combination of a function and the lexical environment in which that function was created. When the function is used elsewhere, references to outer bindings still resolve against that environment.
02Why it matters
Closures power callbacks, event handlers, module privacy, memoization, and function factories. They also explain stale values and accidental memory retention.
03Mental model
Think in bindings, not copied values. A returned function holds a reference to its lexical environment. Independent calls create independent environments.
STEP 1 / 4
Create outer environment
makeCounter begins with a new count binding.
04Visual walkthrough
function makeCounter(start = 0) { let count = start; return { increment: () => ++count, current: () => count, };}
const first = makeCounter(2);const second = makeCounter(10);first and second do not share count. Each call created a new environment. The two methods returned by one call do share its binding.
05Code example: private state
function createRequestCache() { const entries = new Map(); return async function cached(key, load) { if (!entries.has(key)) entries.set(key, load()); return entries.get(key); };}06A strong interview explanation
Coaching note: start with lexical scope. State that functions resolve free variables where they were defined, not where they are called. Then show that an outer environment can remain reachable after its execution completes. Close with a concrete use case and one trade-off.
Follow-up: Does a closure capture every variable?
Conceptually it closes over its lexical environment, but engines can optimize storage when observable behavior is unchanged. Avoid making memory-layout guarantees.
07Common traps
- Describing closure as a snapshot. Bindings can change.
- Assuming loop callbacks always share one binding; let creates per-iteration bindings.
- Retaining large object graphs through long-lived listeners.
08Check your understanding
Two counters come from two calls to the same factory. Do they share state?
No. Each invocation creates a distinct lexical environment. Methods returned by the same invocation can share state.
09Summary
Closures are a consequence of lexical scoping. Reason about environment identity, binding lifetime, and reachability; those three ideas handle most interview follow-ups.
DDConcept deep dives
Deep dive 1
Lexical environments hold bindings
When JavaScript creates a function, the function receives an internal reference to the lexical environment that surrounds its definition. Identifier lookup starts in the current environment and follows outer links. If a returned callback still needs an outer binding, that environment remains reachable after the outer call returns. The callback does not receive a frozen snapshot: it observes the same binding as it changes.
- Each call to an outer function creates an independent environment.
- Several inner functions created by one call can share the same binding.
- Only reachable data is retained; an engine may optimize unused bindings.
function createBox(value) { return { read: () => value, write: (next) => { value = next; }, };}
const box = createBox('first');box.write('second');console.log(box.read()); // secondBoth methods close over one value binding. write reassigns that binding, so read observes the latest value rather than the value present when read was created.
Deep dive 2
Factories create privacy and specialization
Closures are useful when configuration is known earlier than execution. A factory receives stable dependencies once and returns a smaller function for repeated use. The captured values remain private because consumers only receive the returned API. This pattern powers configured loggers, validators, memoizers, event handlers, and module-like state without exposing mutable variables globally.
- Capture stable configuration at the lifecycle boundary where it becomes known.
- Expose behavior rather than returning the mutable object you intended to protect.
- Prefer explicit arguments when hidden dependencies would make testing or ownership unclear.
Deep dive 3
Retention and stale snapshots are ownership problems
A closure keeps data alive only while something reachable keeps the closure alive. Long-lived listeners, timers, observers, promises, and caches can unintentionally retain captured objects. In render-based frameworks, each render also creates new callbacks over that render's values; delayed work can therefore see an older snapshot. The fix depends on ownership: release obsolete callbacks, synchronize dependencies, or use an intentionally current mutable reference.
- Use heap retaining paths to identify the real owner of leaked data.
- Clean up a resource at the same lifecycle boundary where it was registered.
- Do not replace every closure with global state; narrow what is captured and define lifetime.
QAInterview questions and model answers
Attempt each answer aloud before opening it. The model answer shows the depth and precision expected in an interview; it is not a script to memorize.
Intermediate · Coding · 1 min · Question 1What is a closure, precisely?Open model answer
Model answer
A closure is a function together with the lexical environment in which it was created. The function keeps access to the bindings it references from that environment even when it executes outside the creating scope. It retains bindings, not frozen copies of their values.
function makeCounter() { let count = 0; // lives in makeCounter's scope return () => ++count; // the returned fn closes over `count`}
const next = makeCounter();next(); // 1next(); // 2 — `count` survived after makeCounter() returnedBeginner · Conceptual · 1 min · Question 2Does every JavaScript function create a closure?Open model answer
Model answer
Every function is created with a reference to its lexical environment, but the closure becomes practically observable when it references outer bindings and survives beyond the immediate execution of that outer scope.
Open question page →Intermediate · Coding · 1 min · Question 3Why do closures in a loop sometimes produce the same result?Open model answer
Model answer
A loop declared with var has one function-scoped binding, so every callback closes over that same binding and observes its final value. let creates a fresh per-iteration binding. A factory or IIFE also works by creating a new lexical environment for each callback.
for (var i = 0; i < 3; i++) { setTimeout(() => console.log(i)); // 3, 3, 3 — one shared `i`}
for (let j = 0; j < 3; j++) { setTimeout(() => console.log(j)); // 0, 1, 2 — fresh `j` per iteration}Advanced · Coding · 1 min · Question 4Can closures cause memory leaks?Open model answer
Model answer
A reachable closure keeps its referenced environment reachable. That is intentional, but retaining a long-lived callback can also retain large objects unnecessarily. Remove obsolete listeners and timers, and avoid capturing more state than the callback needs.
function attach(node) { const big = new Array(1_000_000).fill('x'); // captured by `handler` const handler = () => console.log(big.length); node.addEventListener('click', handler); return () => node.removeEventListener('click', handler); // call on teardown}Intermediate · Coding · 1 min · Question 5How are closures used for encapsulation?Open model answer
Model answer
A factory can keep mutable bindings private and expose only functions that operate on them. Consumers cannot access the bindings directly, but returned methods share the same environment. ES private fields are often clearer for class-shaped objects, while closures fit functional APIs.
function createAccount(balance) { return { deposit: (n) => (balance += n), getBalance: () => balance, };}
const acct = createAccount(100);acct.deposit(50); // 150acct.getBalance(); // 150acct.balance; // undefined — no direct accessAdvanced · Conceptual · 1 min · Question 6What is a stale closure in React?Open model answer
Model answer
A callback created during a render sees the props and state from that render. If an effect or delayed callback continues using it after values change, it may observe an old snapshot. Correct dependencies, functional state updates, or a ref for deliberately mutable current data solve different versions of the problem.
Likely follow-ups
Does adding every value to an effect dependency list always solve it?
It keeps the effect synchronized, but may reveal that the effect owns the wrong responsibility. Sometimes the better fix is deriving during render, moving logic into the event, or using a functional update.
Intermediate · Conceptual · 1 min · Question 7Do closures capture objects by reference and primitives by value?Open model answer
Model answer
That wording is misleading. Closures reference lexical bindings. A binding may currently contain a primitive or an object reference, and reassignment changes what later reads observe.
Open question page →Advanced · Conceptual · 1 min · Question 8How can a closure implement memoization?Open model answer
Model answer
The wrapper closes over a cache keyed by arguments, returns a stored result for a hit, and computes on a miss. Production design must bound memory and define key equality.
Open question page →Beginner · Conceptual · 1 min · Question 9What is the module pattern?Open model answer
Model answer
An immediately invoked function or factory keeps implementation bindings private and returns a public object of closures. ES modules now provide clearer file-level encapsulation for many uses.
Open question page →Intermediate · Conceptual · 1 min · Question 10How would you test closure-backed private state?Open model answer
Model answer
Test the observable API and independence between factory instances. Avoid reaching into private bindings; verify allowed transitions, invalid inputs, and that separate environments never share state.
Open question page →SCScenario questions
Scenario 1
A page creates thousands of row callbacks and memory keeps growing after rows are removed. How would you investigate?
- Confirm the growth with repeated allocation and garbage-collection profiles.
- Inspect retaining paths to learn which listener, timer, or collection still reaches removed rows.
- Check what each callback captures and whether teardown removes the owning subscription.
- Fix lifecycle ownership, then repeat the same profile to verify objects become collectible.
Reveal worked answer
The closure is not automatically the bug; continued reachability is. I would use heap snapshots to find the root retaining each removed row, commonly an event listener, observer, timer, or cache. Cleanup must happen at the same lifecycle boundary where the resource was registered. Then I would verify the detached nodes and captured models disappear after forced collection.