Closure
Perhaps without realizing it, almost every JS developer has made use of closure. In fact, closure is one of the most pervasive programming functionalities across a majority of languages. It might even be as important to understand as variables or loops; that’s how fundamental it is.
Yet it feels kind of hidden, almost magical. And it’s often talked about in either very abstract or very informal terms, which does little to help us nail down exactly what it is.
We need to be able to recognize where closure is used in programs, as the presence or lack of closure is sometimes the cause of bugs (or even the cause of performance issues).
So let’s define closure in a pragmatic and concrete way:
Closure is when a function remembers and continues to access variables from outside its scope, even when the function is executed in a different scope.
We see two definitional characteristics here. First, closure is part of the nature of a function. Objects don’t get closures, functions do. Second, to observe a closure, you must execute a function in a different scope than where that function was originally defined.
Consider:
function greeting(msg) {
return function who(name) {
console.log(`${ msg }, ${ name }!`);
};
}
var hello = greeting("Hello");
var howdy = greeting("Howdy");
hello("Kyle");
// Hello, Kyle!
hello("Sarah");
// Hello, Sarah!
howdy("Grant");
// Howdy, Grant!
First, the greeting(..)
outer function is executed, creating an instance of the inner function who(..)
; that function closes over the variable msg
, which is the parameter from the outer scope of greeting(..)
. When that inner function is returned, its reference is assigned to the hello
variable in the outer scope. Then we call greeting(..)
a second time, creating a new inner function instance, with a new closure over a new msg
, and return that reference to be assigned to howdy
.
When the greeting(..)
function finishes running, normally we would expect all of its variables to be garbage collected (removed from memory). We’d expect each msg
to go away, but they don’t. The reason is closure. Since the inner function instances are still alive (assigned to hello
and howdy
, respectively), their closures are still preserving the msg
variables.
These closures are not a snapshot of the msg
variable’s value; they are a direct link and preservation of the variable itself. That means closure can actually observe (or make!) updates to these variables over time.
function counter(step = 1) {
var count = 0;
return function increaseCount(){
count = count + step;
return count;
};
}
var incBy1 = counter(1);
var incBy3 = counter(3);
incBy1(); // 1
incBy1(); // 2
incBy3(); // 3
incBy3(); // 6
incBy3(); // 9
Each instance of the inner increaseCount()
function is closed over both the count
and step
variables from its outer counter(..)
function’s scope. step
remains the same over time, but count
is updated on each invocation of that inner function. Since closure is over the variables and not just snapshots of the values, these updates are preserved.
Closure is most common when working with asynchronous code, such as with callbacks. Consider:
function getSomeData(url) {
ajax(url,function onResponse(resp){
console.log(
`Response (from ${ url }): ${ resp }`
);
});
}
getSomeData("https://some.url/wherever");
// Response (from https://some.url/wherever): ...
The inner function onResponse(..)
is closed over url
, and thus preserves and remembers it until the Ajax call returns and executes onResponse(..)
. Even though getSomeData(..)
finishes right away, the url
parameter variable is kept alive in the closure for as long as needed.
It’s not necessary that the outer scope be a function—it usually is, but not always—just that there be at least one variable in an outer scope accessed from an inner function:
for (let [idx,btn] of buttons.entries()) {
btn.addEventListener("click",function onClick(){
console.log(`Clicked on button (${ idx })!`);
});
}
Because this loop is using let
declarations, each iteration gets new block-scoped (aka, local) idx
and btn
variables; the loop also creates a new inner onClick(..)
function each time. That inner function closes over idx
, preserving it for as long as the click handler is set on the btn
. So when each button is clicked, its handler can print its associated index value, because the handler remembers its respective idx
variable.
Remember: this closure is not over the value (like 1
or 3
), but over the variable idx
itself.
Closure is one of the most prevalent and important programming patterns in any language. But that’s especially true of JS; it’s hard to imagine doing anything useful without leveraging closure in one way or another.
If you’re still feeling unclear or shaky about closure, the majority of Book 2, Scope & Closures is focused on the topic.