See the Closure
Closure is originally a mathematical concept, from lambda calculus. But I’m not going to list out math formulas or use a bunch of notation and jargon to define it.
Instead, I’m going to focus on a practical perspective. We’ll start by defining closure in terms of what we can observe in different behavior of our programs, as opposed to if closure was not present in JS. However, later in this chapter, we’re going to flip closure around to look at it from an alternative perspective.
Closure is a behavior of functions and only functions. If you aren’t dealing with a function, closure does not apply. An object cannot have closure, nor does a class have closure (though its functions/methods might). Only functions have closure.
For closure to be observed, a function must be invoked, and specifically it must be invoked in a different branch of the scope chain from where it was originally defined. A function executing in the same scope it was defined would not exhibit any observably different behavior with or without closure being possible; by the observational perspective and definition, that is not closure.
Let’s look at some code, annotated with its relevant scope bubble colors (see Chapter 2):
// outer/global scope: RED(1)
function lookupStudent(studentID) {
// function scope: BLUE(2)
var students = [
{ id: 14, name: "Kyle" },
{ id: 73, name: "Suzy" },
{ id: 112, name: "Frank" },
{ id: 6, name: "Sarah" }
];
return function greetStudent(greeting){
// function scope: GREEN(3)
var student = students.find(
student => student.id == studentID
);
return `${ greeting }, ${ student.name }!`;
};
}
var chosenStudents = [
lookupStudent(6),
lookupStudent(112)
];
// accessing the function's name:
chosenStudents[0].name;
// greetStudent
chosenStudents[0]("Hello");
// Hello, Sarah!
chosenStudents[1]("Howdy");
// Howdy, Frank!
The first thing to notice about this code is that the lookupStudent(..)
outer function creates and returns an inner function called greetStudent(..)
. lookupStudent(..)
is called twice, producing two separate instances of its inner greetStudent(..)
function, both of which are saved into the chosenStudents
array.
We verify that’s the case by checking the .name
property of the returned function saved in chosenStudents[0]
, and it’s indeed an instance of the inner greetStudent(..)
.
After each call to lookupStudent(..)
finishes, it would seem like all its inner variables would be discarded and GC’d (garbage collected). The inner function is the only thing that seems to be returned and preserved. But here’s where the behavior differs in ways we can start to observe.
While greetStudent(..)
does receive a single argument as the parameter named greeting
, it also makes reference to both students
and studentID
, identifiers which come from the enclosing scope of lookupStudent(..)
. Each of those references from the inner function to the variable in an outer scope is called a closure. In academic terms, each instance of greetStudent(..)
closes over the outer variables students
and studentID
.
So what do those closures do here, in a concrete, observable sense?
Closure allows greetStudent(..)
to continue to access those outer variables even after the outer scope is finished (when each call to lookupStudent(..)
completes). Instead of the instances of students
and studentID
being GC’d, they stay around in memory. At a later time when either instance of the greetStudent(..)
function is invoked, those variables are still there, holding their current values.
If JS functions did not have closure, the completion of each lookupStudent(..)
call would immediately tear down its scope and GC the students
and studentID
variables. When we later called one of the greetStudent(..)
functions, what would then happen?
If greetStudent(..)
tried to access what it thought was a BLUE(2) marble, but that marble did not actually exist (anymore), the reasonable assumption is we should get a ReferenceError
, right?
But we don’t get an error. The fact that the execution of chosenStudents[0]("Hello")
works and returns us the message “Hello, Sarah!”, means it was still able to access the students
and studentID
variables. This is a direct observation of closure!
Pointed Closure
Actually, we glossed over a little detail in the previous discussion which I’m guessing many readers missed!
Because of how terse the syntax for =>
arrow functions is, it’s easy to forget that they still create a scope (as asserted in “Arrow Functions” in Chapter 3). The student => student.id == studentID
arrow function is creating another scope bubble inside the greetStudent(..)
function scope.
Building on the metaphor of colored buckets and bubbles from Chapter 2, if we were creating a colored diagram for this code, there’s a fourth scope at this innermost nesting level, so we’d need a fourth color; perhaps we’d pick ORANGE(4) for that scope:
var student = students.find(
student =>
// function scope: ORANGE(4)
student.id == studentID
);
The BLUE(2) studentID
reference is actually inside the ORANGE(4) scope rather than the GREEN(3) scope of greetStudent(..)
; also, the student
parameter of the arrow function is ORANGE(4), shadowing the GREEN(3) student
.
The consequence here is that this arrow function passed as a callback to the array’s find(..)
method has to hold the closure over studentID
, rather than greetStudent(..)
holding that closure. That’s not too big of a deal, as everything still works as expected. It’s just important not to skip over the fact that even tiny arrow functions can get in on the closure party.
Adding Up Closures
Let’s examine one of the canonical examples often cited for closure:
function adder(num1) {
return function addTo(num2){
return num1 + num2;
};
}
var add10To = adder(10);
var add42To = adder(42);
add10To(15); // 25
add42To(9); // 51
Each instance of the inner addTo(..)
function is closing over its own num1
variable (with values 10
and 42
, respectively), so those num1
‘s don’t go away just because adder(..)
finishes. When we later invoke one of those inner addTo(..)
instances, such as the add10To(15)
call, its closed-over num1
variable still exists and still holds the original 10
value. The operation is thus able to perform 10 + 15
and return the answer 25
.
An important detail might have been too easy to gloss over in that previous paragraph, so let’s reinforce it: closure is associated with an instance of a function, rather than its single lexical definition. In the preceding snippet, there’s just one inner addTo(..)
function defined inside adder(..)
, so it might seem like that would imply a single closure.
But actually, every time the outer adder(..)
function runs, a new inner addTo(..)
function instance is created, and for each new instance, a new closure. So each inner function instance (labeled add10To(..)
and add42To(..)
in our program) has its own closure over its own instance of the scope environment from that execution of adder(..)
.
Even though closure is based on lexical scope, which is handled at compile time, closure is observed as a runtime characteristic of function instances.
Live Link, Not a Snapshot
In both examples from the previous sections, we read the value from a variable that was held in a closure. That makes it feel like closure might be a snapshot of a value at some given moment. Indeed, that’s a common misconception.
Closure is actually a live link, preserving access to the full variable itself. We’re not limited to merely reading a value; the closed-over variable can be updated (re-assigned) as well! By closing over a variable in a function, we can keep using that variable (read and write) as long as that function reference exists in the program, and from anywhere we want to invoke that function. This is why closure is such a powerful technique used widely across so many areas of programming!
Figure 4 depicts the function instances and scope links:
Fig. 4: Visualizing Closures
As shown in Figure 4, each call to adder(..)
creates a new BLUE(2) scope containing a num1
variable, as well as a new instance of addTo(..)
function as a GREEN(3) scope. Notice that the function instances (addTo10(..)
and addTo42(..)
) are present in and invoked from the RED(1) scope.
Now let’s examine an example where the closed-over variable is updated:
function makeCounter() {
var count = 0;
return function getCurrent() {
count = count + 1;
return count;
};
}
var hits = makeCounter();
// later
hits(); // 1
// later
hits(); // 2
hits(); // 3
The count
variable is closed over by the inner getCurrent()
function, which keeps it around instead of it being subjected to GC. The hits()
function calls access and update this variable, returning an incrementing count each time.
Though the enclosing scope of a closure is typically from a function, that’s not actually required; there only needs to be an inner function present inside an outer scope:
var hits;
{ // an outer scope (but not a function)
let count = 0;
hits = function getCurrent(){
count = count + 1;
return count;
};
}
hits(); // 1
hits(); // 2
hits(); // 3
NOTE: |
---|
I deliberately defined getCurrent() as a function expression instead of a function declaration. This isn’t about closure, but with the dangerous quirks of FiB (Chapter 6). |
Because it’s so common to mistake closure as value-oriented instead of variable-oriented, developers sometimes get tripped up trying to use closure to snapshot-preserve a value from some moment in time. Consider:
var studentName = "Frank";
var greeting = function hello() {
// we are closing over `studentName`,
// not "Frank"
console.log(
`Hello, ${ studentName }!`
);
}
// later
studentName = "Suzy";
// later
greeting();
// Hello, Suzy!
By defining greeting()
(aka, hello()
) when studentName
holds the value "Frank"
(before the re-assignment to "Suzy"
), the mistaken assumption is often that the closure will capture "Frank"
. But greeting()
is closed over the variable studentName
, not its value. Whenever greeting()
is invoked, the current value of the variable ("Suzy"
, in this case) is reflected.
The classic illustration of this mistake is defining functions inside a loop:
var keeps = [];
for (var i = 0; i < 3; i++) {
keeps[i] = function keepI(){
// closure over `i`
return i;
};
}
keeps[0](); // 3 -- WHY!?
keeps[1](); // 3
keeps[2](); // 3
NOTE: |
---|
This kind of closure illustration typically uses a setTimeout(..) or some other callback like an event handler, inside the loop. I’ve simplified the example by storing function references in an array, so that we don’t need to consider asynchronous timing in our analysis. The closure principle is the same, regardless. |
You might have expected the keeps[0]()
invocation to return 0
, since that function was created during the first iteration of the loop when i
was 0
. But again, that assumption stems from thinking of closure as value-oriented rather than variable-oriented.
Something about the structure of a for
-loop can trick us into thinking that each iteration gets its own new i
variable; in fact, this program only has one i
since it was declared with var
.
Each saved function returns 3
, because by the end of the loop, the single i
variable in the program has been assigned 3
. Each of the three functions in the keeps
array do have individual closures, but they’re all closed over that same shared i
variable.
Of course, a single variable can only ever hold one value at any given moment. So if you want to preserve multiple values, you need a different variable for each.
How could we do that in the loop snippet? Let’s create a new variable for each iteration:
var keeps = [];
for (var i = 0; i < 3; i++) {
// new `j` created each iteration, which gets
// a copy of the value of `i` at this moment
let j = i;
// the `i` here isn't being closed over, so
// it's fine to immediately use its current
// value in each loop iteration
keeps[i] = function keepEachJ(){
// close over `j`, not `i`!
return j;
};
}
keeps[0](); // 0
keeps[1](); // 1
keeps[2](); // 2
Each function is now closed over a separate (new) variable from each iteration, even though all of them are named j
. And each j
gets a copy of the value of i
at that point in the loop iteration; that j
never gets re-assigned. So all three functions now return their expected values: 0
, 1
, and 2
!
Again remember, even if we were using asynchrony in this program, such as passing each inner keepEachJ()
function into setTimeout(..)
or some event handler subscription, the same kind of closure behavior would still be observed.
Recall the “Loops” section in Chapter 5, which illustrates how a let
declaration in a for
loop actually creates not just one variable for the loop, but actually creates a new variable for each iteration of the loop. That trick/quirk is exactly what we need for our loop closures:
var keeps = [];
for (let i = 0; i < 3; i++) {
// the `let i` gives us a new `i` for
// each iteration, automatically!
keeps[i] = function keepEachI(){
return i;
};
}
keeps[0](); // 0
keeps[1](); // 1
keeps[2](); // 2
Since we’re using let
, three i
‘s are created, one for each loop, so each of the three closures just work as expected.
Common Closures: Ajax and Events
Closure is most commonly encountered with callbacks:
function lookupStudentRecord(studentID) {
ajax(
`https://some.api/student/${ studentID }`,
function onRecord(record) {
console.log(
`${ record.name } (${ studentID })`
);
}
);
}
lookupStudentRecord(114);
// Frank (114)
The onRecord(..)
callback is going to be invoked at some point in the future, after the response from the Ajax call comes back. This invocation will happen from the internals of the ajax(..)
utility, wherever that comes from. Furthermore, when that happens, the lookupStudentRecord(..)
call will long since have completed.
Why then is studentID
still around and accessible to the callback? Closure.
Event handlers are another common usage of closure:
function listenForClicks(btn,label) {
btn.addEventListener("click",function onClick(){
console.log(
`The ${ label } button was clicked!`
);
});
}
var submitBtn = document.getElementById("submit-btn");
listenForClicks(submitBtn,"Checkout");
The label
parameter is closed over by the onClick(..)
event handler callback. When the button is clicked, label
still exists to be used. This is closure.
What If I Can’t See It?
You’ve probably heard this common adage:
If a tree falls in the forest but nobody is around to hear it, does it make a sound?
It’s a silly bit of philosophical gymnastics. Of course from a scientific perspective, sound waves are created. But the real point: does it matter if the sound happens?
Remember, the emphasis in our definition of closure is observability. If a closure exists (in a technical, implementation, or academic sense) but it cannot be observed in our programs, does it matter? No.
To reinforce this point, let’s look at some examples that are not observably based on closure.
For example, invoking a function that makes use of lexical scope lookup:
function say(myName) {
var greeting = "Hello";
output();
function output() {
console.log(
`${ greeting }, ${ myName }!`
);
}
}
say("Kyle");
// Hello, Kyle!
The inner function output()
accesses the variables greeting
and myName
from its enclosing scope. But the invocation of output()
happens in that same scope, where of course greeting
and myName
are still available; that’s just lexical scope, not closure.
Any lexically scoped language whose functions didn’t support closure would still behave this same way.
In fact, global scope variables essentially cannot be (observably) closed over, because they’re always accessible from everywhere. No function can ever be invoked in any part of the scope chain that is not a descendant of the global scope.
Consider:
var students = [
{ id: 14, name: "Kyle" },
{ id: 73, name: "Suzy" },
{ id: 112, name: "Frank" },
{ id: 6, name: "Sarah" }
];
function getFirstStudent() {
return function firstStudent(){
return students[0].name;
};
}
var student = getFirstStudent();
student();
// Kyle
The inner firstStudent()
function does reference students
, which is a variable outside its own scope. But since students
happens to be from the global scope, no matter where that function is invoked in the program, its ability to access students
is nothing more special than normal lexical scope.
All function invocations can access global variables, regardless of whether closure is supported by the language or not. Global variables don’t need to be closed over.
Variables that are merely present but never accessed don’t result in closure:
function lookupStudent(studentID) {
return function nobody(){
var msg = "Nobody's here yet.";
console.log(msg);
};
}
var student = lookupStudent(112);
student();
// Nobody's here yet.
The inner function nobody()
doesn’t close over any outer variables—it only uses its own variable msg
. Even though studentID
is present in the enclosing scope, studentID
is not referred to by nobody()
. The JS engine doesn’t need to keep studentID
around after lookupStudent(..)
has finished running, so GC wants to clean up that memory!
Whether JS functions support closure or not, this program would behave the same. Therefore, no observed closure here.
If there’s no function invocation, closure can’t be observed:
function greetStudent(studentName) {
return function greeting(){
console.log(
`Hello, ${ studentName }!`
);
};
}
greetStudent("Kyle");
// nothing else happens
This one’s tricky, because the outer function definitely does get invoked. But the inner function is the one that could have had closure, and yet it’s never invoked; the returned function here is just thrown away. So even if technically the JS engine created closure for a brief moment, it was not observed in any meaningful way in this program.
A tree may have fallen… but we didn’t hear it, so we don’t care.
Observable Definition
We’re now ready to define closure:
Closure is observed when a function uses variable(s) from outer scope(s) even while running in a scope where those variable(s) wouldn’t be accessible.
The key parts of this definition are:
Must be a function involved
Must reference at least one variable from an outer scope
Must be invoked in a different branch of the scope chain from the variable(s)
This observation-oriented definition means we shouldn’t dismiss closure as some indirect, academic trivia. Instead, we should look and plan for the direct, concrete effects closure has on our program behavior.