Before you begin
Why this matters
Every function you store for later, such as one you pass to another function, works this way. If you know what it keeps from the place where it was written, you can predict what it returns when it finally runs.
What this helps you reason about
- Predict what a stored function returns when it is called later.
- Tell whether a function sees a change made after it was created.
- Spot functions made in a loop that all show the same value.
A function can use variables from the code around it, and it can keep using them after that code has finished. This lesson shows what such a function keeps, when it sees later changes, and what goes wrong when several functions share one variable.
Already know this? The short version
- A closure captures variables, not values: it reads each one when it runs, so it sees changes made after it was created. A parameter is a separate variable, so a closure that captured a parameter keeps the value passed in.
- Each call to a function makes new locals, so closures returned by two calls have separate state; closures made in one call share it.
- A for loop whose header declares let makes a new counter for each pass; a variable declared outside the loop is one variable for every closure made in the loop.
- A closure keeps everything it captures reachable; when it needs only a small part of a large value, copy that part into its own variable and capture that.
Words in this lesson (5)
- closure
- A function together with the variables from the surrounding code that it uses. Those variables last as long as the function does.
- capture
- To use a variable from the surrounding code inside a closure. A TypeScript closure keeps the variable itself, so it sees every later change to that variable.
- arrow function
- A function written as a value, without a name, such as () => "stall " + i. The part before => lists its parameters, and the part after it is the value it returns.
- lexical scope
- The rule that a name refers to a declaration in the code around the place where the name is written: its own block, an enclosing block or function, or the top level of the file. Which code calls a function makes no difference.
- garbage collector
- The part of the JavaScript engine that finds values that are no longer reachable and reuses their memory, at a time it chooses.
Examples are focused snippets; imports, enclosing functions, or classes may be omitted. C examples use C11; Haskell examples use Haskell 2010. Expand the notes for language-specific depth.
01 / Closures
How can a function use a name after its call has returned?
Function calls showed that a function is a value: it can be stored in a variable and called later. A function can also be made inside another function and returned. makeTracker below returns the function drink. Its return type, () => number, means “a function that takes nothing and returns a number”.
drink uses glasses, a local variable of makeTracker. When makeTracker returns, its call is over, but drink still uses glasses, so glasses does not end with the call. A function together with the variables it uses from the code where it was written is called a closure. We say the closure captures those variables. Which variables those are follows lexical scope, as in Scope and shadowing: the code around the function, not the code that calls it.
Each call to makeTracker runs let glasses = 0 again and makes a new variable. So ana and ben are two closures with two separate counts.
function makeTracker(): () => number {
let glasses = 0;
return function drink(): number {
glasses += 1;
return glasses;
};
}
const ana = makeTracker();
const ben = makeTracker();
console.log(ana(), ana(), ben()); // 1 2 1 A closure keeps the variables it uses; each call to the maker makes new ones.
Coming from Python: A Python closure needs nonlocal glasses before it can assign to glasses. A TypeScript closure can assign to a captured variable directly.
Precisely: what the specification says
Each function object records the environment where it was created. A call to the function makes a new environment whose outer environment is that recorded one, which is how drink finds glasses. While drink can be reached, so can glasses. Stack and heap, in the Runtimes module, follows a counter like this one in the frame list and explains where such variables are kept.
Experiment
Make two trackers from one maker
Each call to makeTracker runs let glasses = 0 and makes a new variable. drink captures the glasses of the call that made it, so ana and ben count separately. After the edit there is one glasses for the whole file, and both closures capture it.
Why it matters Two closures have separate state only when each one captured its own variables, made by its own call.
Predict the three numbers. Then move the line let glasses = 0; out of makeTracker, to the line above function makeTracker, and predict again.
function makeTracker(): () => number {
let glasses = 0;
return function drink(): number {
glasses += 1;
return glasses;
};
}
const ana = makeTracker();
const ben = makeTracker();
console.log(ana(), ana(), ben());
What to look for
Step into drink each time. Its frame lists glasses, although the makeTracker frame that declared glasses is gone: 0, then 1 in the first call; 1, then 2 in the second; and 0 again in the third call, which is ben’s. The panel shows ana and ben as function drink(): it names the function, not what the function captured. The run prints 1 2 1. After the edit, <top level> lists glasses too, ben’s call starts from 2, and the run prints 1 2 3. <top level> is the stepper’s name for the code outside every function. Step sometimes stops on the same line two or three times; that is normal for this debugger.
Expected output
1 2 102 / Capture
Does a closure keep the variable or a copy of its value?
A TypeScript closure captures the variable itself, not a copy of its value. It reads the variable when it runs. So when the variable changes after the closure is made, the closure sees the new value: live below sees goal = 10.
A parameter is a new variable. makeCheck(goal) gives target a copy of the value goal holds at the moment of the call, as in Arguments, values, and references. The closure inside makeCheck captures target, not goal, so it keeps 8.
function makeCheck(target: number): (glasses: number) => boolean {
return (glasses) => glasses >= target;
}
let goal = 8;
const live = (glasses: number): boolean => glasses >= goal;
const fixed = makeCheck(goal);
goal = 10;
console.log(live(9), fixed(9)); // false true A closure reads a captured variable when it runs. To keep the value from the time it was made, capture a variable that is never changed afterwards.
Coming from Java: There is no effectively final rule: goal can be assigned after live captured it, and live sees the new value.
Precisely: const and objects
If a closure captures a const that holds an object, the closure still sees changes to the object’s properties: const stops only assignment to the variable, as Mutability and constants showed. A copy of a number is safe from later changes; a copy of a reference is not.
Experiment
Change a variable after two closures captured it
live captured goal itself, and goal is 10 when live runs. The closure from makeCheck captured target, a parameter that received a copy of 8 when makeCheck was called. Changing goal later does not change target.
Why it matters To predict what a closure sees, find the variable it captured and ask what that variable holds when the closure runs.
Predict both answers. Then move the line goal = 10; up, above const fixed = makeCheck(goal);, and predict again.
function makeCheck(target: number): (glasses: number) => boolean {
return (glasses) => glasses >= target;
}
let goal = 8;
const live = (glasses: number): boolean => glasses >= goal;
const fixed = makeCheck(goal);
goal = 10;
console.log(live(9), fixed(9));
What to look for
Step past goal = 10, then into live: its frame lists glasses 9 and goal 10. Step into the closure that makeCheck made: the frame list names it arrow function (line 2), and it lists glasses 9 and target 8, and also goal 10, the top-level variable it can reach. The run prints false true. After the edit, target receives 10, and the run prints false false.
Expected output
false true03 / Loops
What do closures made in a loop share?
Closures made in a loop are a common source of surprises. In the program below, each pass of the loop adds a new arrow function to the array reminders. reminders.push(f) adds f at the end of the array, reminders[0]() calls the first function in it, and the type (() => number)[] means an array of such functions.
All three closures capture hour, which is declared once, before the loop. They run after the loop, when hour is 14, so all three return 14.
The counter n is different. A for loop whose header declares let makes a new n for each pass, so a closure that used n would keep its own pass’s number. TypeScript var, hoisting, and the temporal dead zone shows the same loop with var, where every closure shares one counter. A const declared inside the loop body is also new for each pass.
const reminders: (() => number)[] = [];
let hour = 8;
for (let n = 0; n < 3; n++) {
reminders.push(() => hour);
hour = hour + 2;
}
// prints 14 14 14
console.log(reminders[0](), reminders[1](), reminders[2]()); Closures made in a loop share every variable declared outside the loop; a variable declared in the loop header with let, or in the body, is new for each pass.
Coming from Python: A Python for loop has one loop variable for the whole loop, so lambdas made in it all see the last value. A TypeScript for (let …) loop gives each pass its own.
Experiment
Make three reminders in a loop
hour is declared once, before the loop, so all three closures capture the same variable. They run after the loop, when it holds 14. The loop header let n makes a new n for each pass, so a closure that uses n keeps its own pass’s number.
Why it matters Closures made in a loop share every variable declared outside the loop. A variable that is new for each pass, such as a let in the loop header or in the loop body, gives each closure its own.
Predict the three numbers. Then change () => hour to () => 8 + 2 * n and predict again.
const reminders: (() => number)[] = [];
let hour = 8;
for (let n = 0; n < 3; n++) {
reminders.push(() => hour);
hour = hour + 2;
}
console.log(reminders[0](), reminders[1](), reminders[2]());
What to look for
Step through the loop: hour grows from 8 to 14, and reminders fills with three entries, each shown as arrow function (). When reminders[0]() runs, the frame list shows arrow function (line 4), and its frame lists hour 14; the other two calls show the same. The run prints 14 14 14. After the edit, the three calls list n 0, n 1, and n 2, and the run prints 8 10 12.
Expected output
14 14 1404 / Lifetime
How long do captured values stay alive?
A closure keeps every variable it captures, and everything those variables lead to, for as long as the closure itself can be reached. Usually that is what you want. It becomes a problem when a closure that is kept for a long time captures a large value but needs only a small part of it.
makeSummary below uses only the number of entries, but it captures log, so the whole array stays in memory while the closure is kept. makeSmallSummary copies the number first and captures only size. Note that the two do different things: the small one keeps the count from the moment it was made, and does not see entries added later. Reachability and garbage collection, in the Runtimes module, explains how the garbage collector decides what is still reachable.
function makeSummary(log: string[]): () => string {
return () => log.length + " drinks";
}
function makeSmallSummary(log: string[]): () => string {
const size = log.length;
return () => size + " drinks";
} A closure keeps alive everything it captures; capture only what it needs.
Precisely: one record for all closures of a call
In V8, the engine in Node.js, the closures made by one call share one record of captured variables. If one closure from that call captures log, log stays alive while any closure from that call is kept. This is how one engine works, not a rule of the language.
05 / Try it
Can you predict what each closure sees?
makeScale’s parameter has the same name as the top-level variable. Decide which variable each closure captured. The next lesson, Arrays, lists, and vectors, starts the collections that this lesson used in passing.
function makeScale(factor: number): (cups: number) => number {
return (cups) => cups * factor;
}
let factor = 2;
const fixed = makeScale(factor);
const live = (cups: number): number => cups * factor;
factor = 3;
console.log(fixed(5), live(5));
What is printed?
Reveal the reasoning
10 15. fixed is the closure from makeScale(2). It captured makeScale’s parameter factor, which received a copy of 2; inside makeScale, that parameter hides the top-level factor. live captured the top-level factor, which holds 3 when live(5) runs.
Practice tickets
- CLOSURE-101 · Every eye-drop reminder shows the same time → Free
- CLOSURE-102 · Alerts keep using the old limit after the doctor changes it → Sign in · Pro
- CLOSURE-103 · An earlier appointment card shows a later time → Sign in · Pro