When you use an object, variable, or function, you do so on purpose. You think, "This is where I need a variable," and you add it to your code. Closures, however, are something else. While most programmers are starting to learn about closures, these people are already using closures without knowing it. Probably the same thing happens to you. So learning closures is less about learning a new idea than learning how to recognize something that you've come across many times before. In a nutshell, closure is when a function accesses variables declared outside of it. For example, the closure is contained in this piece of code:
let users = ['Alice', 'Dan', 'Jessica'];
let query = 'A';
let user = users.filter(user => user.startsWith(query));
Note that it
user => user.startsWith(query)
is a function. She uses a variable query
. And this variable is declared outside the function. This is a closure.
You can skip reading if you like. The rest of this material looks at closures in a different light. Instead of talking about what closures are, this part of the article will go into the details of detecting closures. This is similar to how the first programmers worked in the 1960s.
Step 1: functions can access variables declared outside of them
To understand closures, you need to be fairly familiar with variables and functions. In this example, we are declaring a variable
food
inside a function eat
:
function eat() {
let food = 'cheese';
console.log(food + ' is good');
}
eat(); // 'cheese is good'
What if you want to be able to change the value of a variable later
food
, outside the function eat
? In order to do this, we can remove the variable itself from the function food
and move it to a higher level:
let food = 'cheese'; //
function eat() {
console.log(food + ' is good');
}
This allows you to change the variable
food
"from the outside" when needed:
eat(); // 'cheese is good'
food = 'pizza';
eat(); // 'pizza is good'
food = 'sushi';
eat(); // 'sushi is good'
In other words, the variable is
food
no longer eat
local to the function . But the function eat
, in spite of this, has no problems when working with this variable. Functions can access variables declared outside of them. Stop for a while and check yourself, make sure that you have no problems with this idea. Once this thought has firmly settled in your mind, move on to the second step.
Step 2: placing the code in the function call
Let's say we have some code:
/* */
It doesn't matter which code it is. But let's say we need to run it twice.
The first way to do this is to just make a copy of the code:
/* */
/* */
Another way is to put the code in a loop:
for (let i = 0; i < 2; i++) {
/* */
}
And the third way, which is especially interesting for us today, is to put this code in a function:
function doTheThing() {
/* */
}
doTheThing();
doTheThing();
Using a function gives us maximum flexibility, since it allows us to call the given code any number of times, at any time and from anywhere in the program.
In fact, if necessary, we can limit ourselves to just a single call of the new function:
function doTheThing() {
/* */
}
doTheThing();
Please note that the above code is equivalent to the original code snippet:
/* */
In other words, if we take some piece of code and "wrap" it in a function, and then call this function exactly once, then we will not affect what exactly this code does. There are some exceptions to this rule, which we will not pay attention to, but, in general, we can assume that this rule is true. Think about it for a while, get used to this idea.
Step 3: detecting closures
We figured out two ideas:
- Functions can work with variables declared outside of them.
- If you place the code in a function and call this function once, it will not affect the results of the code.
Now let's talk about what will happen if these two ideas are combined.
Let's take the sample code we looked at in the first step:
let food = 'cheese';
function eat() {
console.log(food + ' is good');
}
eat();
Now let's put this whole example in a function that we plan to call only once:
function liveADay() {
let food = 'cheese';
function eat() {
console.log(food + ' is good');
}
eat();
}
liveADay();
Read both of the previous code examples and make sure they are equivalent.
The second example works! But let's take a closer look at it. Note that the function
eat
is inside a function liveADay
. Is this allowed in JavaScript? Is it really possible to wrap one function inside another?
There are languages ββin which code structured in this way will turn out to be incorrect. For example, in C, such code would be wrong (there are no closures in this language). This means that when using C, our second conclusion will be wrong β you can't just take an arbitrary piece of code and "wrap" it in a function. But there is no such limitation in JavaScript.
Let's think about this code again, paying special attention to where the variable is declared and where it is used.
food
:
function liveADay() {
let food = 'cheese'; // `food`
function eat() {
console.log(food + ' is good'); // `food`
}
eat();
}
liveADay();
Let's go through this code step by step together. First, we declare, at the top level, a function
liveADay
. We call her immediately. This function has a local variable food
. The function is also declared in it eat
. Then the liveADay
function is called internally eat
. Since the function eat
is inside a function liveADay
, it eat
"sees" all the variables declared in liveADay
. This is why the function eat
can read the value of the variable food
.
This is called closure.
We talk about the existence of a closure when a function (such as
eat
) reads or writes the value of a variable (such as food
), which is declared outside of it (for example, in a function liveADay
).
Think about these words, re-read them. Test yourself by finding what we're talking about in our sample code.
Here is an example that was given at the very beginning of the article:
let users = ['Alice', 'Dan', 'Jessica'];
let query = 'A';
let user = users.filter(user => user.startsWith(query));
It might be easier to notice the closure by rewriting this example using a function expression:
let users = ['Alice', 'Dan', 'Jessica'];
// 1. query
let query = 'A';
let user = users.filter(function(user) {
// 2.
// 3. query ( !)
return user.startsWith(query);
});
When a function accesses a variable declared outside of it, we call it a closure. The term itself is used loosely enough. Some people will call the nested function itself, shown in the example, "closure". Others may refer to an external variable accessor by calling it a "closure". In practice, this does not matter.
Function call ghost
Closures may seem like a deceivingly simple concept to you now. But this does not mean that they lack some non-obvious features. If you think carefully about the fact that a function can read and write the values ββof variables outside of it, it turns out that this has rather serious consequences.
For example, this means that such variables will βliveβ as long as a function nested within another function can be called.
function liveADay() {
let food = 'cheese';
function eat() {
console.log(food + ' is good');
}
// eat
setTimeout(eat, 5000);
}
liveADay();
In this example, it
food
is a local variable inside a function call liveADay()
. I just want to decide that this variable will "disappear" after exiting the function, and will never come back to haunt us like a ghost.
But in the function,
liveADay
we ask the browser to call the function eat
after five seconds. And this function reads the value of the variable food
. As a result, it turns out that the JavaScript engine needs to keep the variable food
associated with the call alive liveADay()
until the function is called eat
.
In this sense, closures can be seen as "ghosts" of past function calls, or as "memories" of such calls. Even though the execution of the function
liveADay()
ended long ago, the variables declared in it must continue to exist as long as the nested function eat
can be called. Fortunately, JavaScript takes care of these mechanisms, so we don't need to do anything special in these situations.
Why are "closures" called that way?
You might be wondering why "closures" are called that way. The reason for this is mainly historical. Anyone familiar with computer jargon might say that an expression like this
user => user.startsWith(query)
has an "open binding". In other words, it is clear from this expression what user
(parameter) is, but when viewed in isolation, it is not clear what it is query
. When we say that it is, in fact, query
a variable that is declared outside of the function, we are βclosingβ that open binding. In other words, we get a closure.
Closures are not implemented in all programming languages. For example, in some languages, like C, you can't use nested functions at all. As a result, the function can work only with its local variables or with global variables. However, there is never a situation in which it can access the local variables of the parent function. This is actually a very unpleasant limitation.
There are also languages ββlike Rust that implement closures. But they use different syntax to describe closures and normal functions. As a result, if you need to read the value of a variable outside of a function, then using Rust you need to use a special construct. The reason for this is that the use of closures may require the internal mechanisms of the language to keep external variables (called the "environment") even after the function call has completed. This additional load on the system is acceptable in JavaScript, but it can, when used in fairly low-level languages, cause performance problems.
Now, I hope you understand the concept of closures in JavaScript.
Are you having difficulty understanding JavaScript concepts?