Closures are commonly utilized in our JavaScript programs, both explicitly and implicitly. Despite being a natural part of the language, many developers are unfamiliar with what closures are and how they function. Rather than learning a new concept, it's about having an "AHA" moment and recognizing something we've been using and doing for a while.
A closure is a function that allows to access/capture/close-over variables from an outer function that has already been returned. In other words, a closure is a function that has access to variables in its outer scope, even after the outer function has returned. Closures are created every time a function is created, at function creation time.
Here we are using a closure and a code snippet you probably are familiar with.
The closure occurs because the inner arrow function retains access to the alice
variable, which is defined in the outer scope of the filter method. This allows the inner function to compare each element of the names array with the value of alice
.
Functions have access to the lexical scope
To understand closures we need to know the relation between variables and functions.
In this example our function sayHello
will capture the name
variable.
And if we change the name variable the function will print the sentence with the new updated name.
Our function has captured the name
variable.
When this program runs, the console prints first Hello, my name is Bob. and then Hello, my name is Alice. Here we can see that the closure has captured the name
variable and not the value itself.
A common mistake when thinking about closures is that they capture the value, but that is not correct. The closure captures the variable itself and not the value.
The sayHello
is a closure since it has captured the variable outside from it's lexical scope.
“Closure is when a function is able to remember and access its lexical scope even when that function is executing outside its lexical scope.” ― Kyle Simpson, You Don't Know JS: Scope & Closures
Why closures?
I like to see closures like a secret box that can hold things for you. You can put things in the box and only you can take them out.
In JavaScript, we use closures to hold onto variables and functions so that they don't get lost or mixed up with other things. This way, we can use them later when we need them.
Picture a magic backpack that can remember items from different places you visit. As you journey through various landscapes, you can pick up unique souvenirs and store them in this enchanted backpack. When you return home and open the backpack, each souvenir brings back memories of the places you collected them from.
Similarly, in JavaScript, a closure is like that magical backpack. It accompanies a function as it travels through different parts of the code, collecting references to variables and data from its surroundings. When the function is executed, it unpacks these memories from the closure, allowing the function to access and use the captured variables, no matter where it's called from. Just as the backpack keeps your cherished memories intact, closures retain the context and state of their enclosing scope, preserving the magic of the past moments as you journey through your code.
Closures and memory
Closures are a powerful tool in JavaScript, but they can also cause problems if you don't use them correctly. One of the most common problems is memory leaks. A memory leak happens when you create a closure that holds onto a variable or function that you don't need anymore. This can happen if you create a closure inside a loop or if you create a closure that holds onto a variable or function that you don't need anymore. The garbage collector can't remove the variable or function from memory because the closure is still holding onto it. This can cause your program to use more memory than it needs to, which can slow down your program or even crash it. let's look at an example of a memory leak caused by a closure:
In this example, the leakyFunction
retains a closure over the data array, even after it's executed.
As a result, the data array cannot be garbage collected, and it will remain in memory even if it's no longer needed.
To avoid memory leaks with closures, it's essential to make sure that any unnecessary references are released or explicitly nullified when they are no longer needed. One common approach to prevent memory leaks is to remove event listeners or clean up any resources within a closure when it's no longer needed.
For example, in a scenario where you have an event listener:
To avoid the memory leak, we could explicitly remove the event listener when it's no longer required:
Here we check the count and if it's greater than 2 we call the cleanup function. And the cleanup function removes the event listener. By doing this, we ensure that the event listener and its associated closure are properly removed, allowing the garbage collector to free up the memory used by the closure and any associated data.
If you have workd with React you are probably familiar with the useEffect
hook.
The useEffect
hook is a good example of how to use closures to avoid memory leaks.
The useEffect
hook allows us to run code when a component is mounted or unmounted.
This is useful for cleaning up any resources that we no longer need.
For example, if we have an event listener that we no longer need, we can use the useEffect
hook to remove the event listener when the component is unmounted.
Here we use the useEffect
hook to add an the scroll event to the window object.
We also use the useEffect
hook to remove the event listener when the component is unmounted.
This ensures that the event listener and its associated closure are properly removed, allowing the garbage collector to free up the memory used by the closure and any associated data.
Closures in other languages
Not all languages have closures. For example, C
does not have closures. In C
, a function can only access variables that are in its own scope. This means that a function cannot access variables from outside its scope, even if those variables are in the same file.
Take Rust
for example, in Rust
we have to explicitly tell the compiler that we want to use a closure. This is because Rust
is a statically typed, low level language, which means that the compiler needs to know the type of every variable at compile time. If we don't tell the compiler that we want to use a closure, it won't know what type of variable we are trying to use, and it will give us an error.
Counter example in Rust
:
The ||
is the closure syntax in Rust
that tells the compiler that we want to use a closure. If we don't use this syntax, the compiler will give us an error.
Summery
Closures in JavaScript are a fascinating and powerful concept that enables developers to write more efficient and maintainable code. I hope this post would help you to understand closures better and how you can use them in your code.