Closures
Introduction
A closure is a function bundled with references to variables from its lexical environment. In plain English: an inner function can keep using variables from an outer function even after the outer function has returned.
If Module 3 has one flagship concept, this is it. Closures power callbacks, data privacy, function factories, memoization, debouncing, and many of the most common JavaScript interview questions.
Why This Matters
Closures are one of the most-tested JavaScript topics at Microsoft, Google, Amazon, Meta, Netflix, and Apple. Interviewers expect you to define them crisply, trace output questions, explain private state, and fix the var loop callback bug. A strong closure explanation signals that your mental model is deeper than syntax.
Theory
Interview-ready definition
A closure is a function together with the lexical environment it was created in, allowing the function to access outer variables even after the outer function has finished executing.
A concise interview answer: A closure is a function that remembers variables from the scope where it was created.
The mental model
When JavaScript creates a function, it records a hidden link to the surrounding lexical environment. If that function is returned, stored in an array, passed as a callback, or called later, the link remains. The outer variables it can still reach stay alive as long as the closure is reachable.
Important nuance: closures capture bindings, not frozen snapshots. If multiple closures point at the same binding, they see the same changing value. If a loop creates a fresh binding per iteration with let, each closure gets a different binding.
The counter factory
The classic example is function makeCounter(){ let c=0; return function(){ return ++c; }; }. Each call to makeCounter creates a new lexical environment with its own c. The returned function closes over that c, so it can increment it later. Two counters are independent because they were created by two different calls.
Private state
Closures can hide variables from the outside world. If balance is local to a factory function and only returned methods can reach it, outside code cannot read or assign balance directly. This is the old-school JavaScript route to encapsulation and still appears in interviews.
The classic var loop bug
With var, a loop has one shared function-scoped binding. Every callback closes over that same binding, so after the loop ends they all see the final value. Fix it by using let (fresh per-iteration binding) or by wrapping each iteration in an IIFE that captures the current value as an argument.
| Pattern | What the closure remembers | Result |
|---|---|---|
| Counter factory | one c per factory call | independent counters |
| Private module | hidden local variables | controlled access |
for (var i...) callbacks | one shared i | all see final value |
for (let i...) callbacks | one i per iteration | each sees its own value |
Visual Diagrams
makeCounter() call
creates environment E1
c = 0
|
v
returns inner function
|
v
counter variable in outer code -----> function object
hidden link -----> E1 { c }
Calling counter() later follows the hidden link and updates c.The outer call is gone from the stack, but the captured environment remains reachable through the returned function.
Code Examples
Counter factory: each call gets private state
This is the closure example every JavaScript interviewer expects you to know.
Private variables through closures
balance is not a property on the returned object. Only the returned methods can access it.
Fixing `var` loop closures with `let` or an IIFE
Both fixes work because they create a separate binding for each callback to close over.
Playground
Press Run to execute the code and see output here.
Output Prediction
Predict the output #1
1function makeCounter() {2 let c = 0;3 return function () {4 return ++c;5 };6}7 8const a = makeCounter();9const b = makeCounter();10 11console.log(a());12console.log(a());13console.log(b());14console.log(a());Predict the output #2
1const fns = [];2 3for (var i = 0; i < 2; i++) {4 fns.push(function () {5 return i;6 });7}8 9for (let j = 0; j < 2; j++) {10 fns.push(function () {11 return j;12 });13}14 15console.log(fns[0](), fns[1](), fns[2](), fns[3]());Coding Exercises
Build a private counter object
MediumImplement createCounter(initial) that returns an object with methods increment(), decrement(), get(), and reset(). The current count must be private: outside code should not be able to read or write it as a property on the returned object.
Interview Questions
1Define a closure in JavaScript. Give a crisp answer an interviewer would accept.
A closure is a function bundled with its lexical environment. It lets the function access variables from the scope where it was created, even after that outer scope has finished executing. Example: a makeCounter function returns an inner function that keeps accessing and updating the outer c variable.
Follow-ups
- Do closures capture values or bindings?
- Why are two counters independent?
2How would you explain a closure to a junior developer?
I would say: imagine a function carries a backpack. When it is created, it puts the outer variables it needs into that backpack by reference. Later, even if the outer function is done, the inner function still has the backpack, so it can keep using those variables. That is why a counter function can remember its count between calls.
Follow-ups
- What is the limitation of the backpack analogy?
- Why do multiple closures sometimes share the same variable?
3How do closures provide private variables?
Declare the variable inside a factory function and return functions that operate on it. The variable is not exposed as an object property or global name, so outside code cannot access it directly. Only the returned closures can read or modify it.
Quiz
1. What does a closure remember?
2. What is printed? `function outer(){ let x = 0; return [function(){ x++; return x; }, function(){ x++; return x; }]; } const pair = outer(); console.log(pair[0]()); console.log(pair[1]());`
3. What is printed? `const f=[]; for(var i=0;i<2;i++){ f.push(function(){ return i; }); } for(let j=0;j<2;j++){ f.push(function(){ return j; }); } console.log(f[0](), f[1](), f[2](), f[3]());`
Summary
- A closure is a function plus access to variables from its lexical environment.
- Closures capture bindings, so shared bindings show updated values, not frozen snapshots.
- Each factory call creates a new environment, which is why two counters are independent.
- Closures enable private state, callbacks, factories, and fixes for classic loop problems.
Cheat Sheet
Definition: a closure is a function bundled with its lexical environment.
Mantra: functions remember where they were created, not where they are called.
Counter: makeCounter returns a function that keeps using c; each factory call gets a new c.
Bindings, not snapshots: multiple closures can share one binding; let loop iterations create fresh bindings.
Private state: keep variables local to a factory and expose methods that close over them.
Loop bug: var = one shared i; fix with let or an IIFE.