Skip to content
Intermediate15 min study

Closures

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

Question progress0 / 10 completed
Start the lesson
Closures visual explanation

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.

Interview-ready: A closure lets a function retain access to bindings from its creation scope, even after the outer function has returned.

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.

Think of it like this: a closure is a backpack a function packs before it leaves home. Even after it travels far away — gets called somewhere else entirely — it still has access to whatever it packed from its original surroundings, because it's carrying that backpack around, not a photograph of what was inside it.

STEP 1 / 4

Create outer environment

makeCounter begins with a new count binding.

04Visual walkthrough

JavaScript
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

JavaScript
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

How to say it out loud: "A closure is what happens when a function keeps access to variables from the scope it was created in, even after that outer function has already returned. It works because JavaScript resolves variables based on where code is written, not where it's called from — that's lexical scoping — so the returned function still holds a live reference to its outer environment instead of losing it. A classic use case is a counter factory: each call creates its own private count variable that only the returned functions can see or change, which gives you encapsulation without needing a class."

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.
JavaScript
function createBox(value) {  return {    read: () => value,    write: (next) => { value = next; },  };}
const box = createBox('first');box.write('second');console.log(box.read()); // second

Both 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.

JavaScript
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() returned
Open question page →
Beginner · 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.

JavaScript
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}
Open question page →
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.

JavaScript
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}
Open question page →
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.

JavaScript
function createAccount(balance) {  return {    deposit: (n) => (balance += n),    getBalance: () => balance,  };}
const acct = createAccount(100);acct.deposit(50);      // 150acct.getBalance();     // 150acct.balance;          // undefined — no direct access
Open question page →
Advanced · 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.

Open question page →
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?

  1. Confirm the growth with repeated allocation and garbage-collection profiles.
  2. Inspect retaining paths to learn which listener, timer, or collection still reaches removed rows.
  3. Check what each callback captures and whether teardown removes the owning subscription.
  4. 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.

Verify and go deeper