Marcell Ciszek Druzynski

Closures

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.

August 07, 2023

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.

1const names = ["Alice", "Bob", "Charlie", "Diana"];
2const alice = "Alice";
3const namesWithoutAlice = names.filter((name) => name !== alice);
1const names = ["Alice", "Bob", "Charlie", "Diana"];
2const alice = "Alice";
3const namesWithoutAlice = names.filter((name) => name !== alice);

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.

1let name = "Bob";
2function sayHello() {
3 let sentence = `Hello, my name is ${name}.`;
4 console.log(sentence);
5 return sentence;
6}
7
8sayHello();
9name = "Alice";
10sayHello();
1let name = "Bob";
2function sayHello() {
3 let sentence = `Hello, my name is ${name}.`;
4 console.log(sentence);
5 return sentence;
6}
7
8sayHello();
9name = "Alice";
10sayHello();

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:

1function createLeakyClosure() {
2 const data = []; // A large array or some other data structure
3
4 return function () {
5 // This function has a closure over the 'data' array
6 // and retains a reference to it even after it's called.
7 console.log(data.length);
8 };
9}
10
11const leakyFunction = createLeakyClosure();
12leakyFunction(); // Logs the length of the 'data' array
13
14// Since 'leakyFunction' has a closure over 'data',
15// the 'data' array cannot be garbage collected.
1function createLeakyClosure() {
2 const data = []; // A large array or some other data structure
3
4 return function () {
5 // This function has a closure over the 'data' array
6 // and retains a reference to it even after it's called.
7 console.log(data.length);
8 };
9}
10
11const leakyFunction = createLeakyClosure();
12leakyFunction(); // Logs the length of the 'data' array
13
14// Since 'leakyFunction' has a closure over 'data',
15// the 'data' array cannot be garbage collected.

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:

1function createEventListener() {
2 const element = document.getElementById("myButton");
3
4 // This function adds an event listener to the element
5 element.addEventListener("click", function () {
6 console.log("Button clicked!");
7 });
8}
9
10createEventListener();
11// If the element with ID 'myButton' is removed from the DOM,
12// the event listener will still persist and create a memory leak.
1function createEventListener() {
2 const element = document.getElementById("myButton");
3
4 // This function adds an event listener to the element
5 element.addEventListener("click", function () {
6 console.log("Button clicked!");
7 });
8}
9
10createEventListener();
11// If the element with ID 'myButton' is removed from the DOM,
12// the event listener will still persist and create a memory leak.

To avoid the memory leak, we could explicitly remove the event listener when it's no longer required:

1let count = 0;
2function createEventListener() {
3 const element = document.getElementById("myButton");
4
5 function onClickHandler() {
6 count++;
7 console.log("Button clicked!");
8 checkCount();
9 }
10
11 element.addEventListener("click", onClickHandler);
12
13 // Clean up the event listener when it's no longer needed
14 function cleanup() {
15 console.log("Cleaned up!");
16 element.removeEventListener("click", onClickHandler);
17 }
18
19 return cleanup;
20}
21
22const cleanupFunction = createEventListener();
23function checkCount() {
24 console.log("count", count);
25 if (count > 2) {
26 cleanupFunction();
27 }
28}
1let count = 0;
2function createEventListener() {
3 const element = document.getElementById("myButton");
4
5 function onClickHandler() {
6 count++;
7 console.log("Button clicked!");
8 checkCount();
9 }
10
11 element.addEventListener("click", onClickHandler);
12
13 // Clean up the event listener when it's no longer needed
14 function cleanup() {
15 console.log("Cleaned up!");
16 element.removeEventListener("click", onClickHandler);
17 }
18
19 return cleanup;
20}
21
22const cleanupFunction = createEventListener();
23function checkCount() {
24 console.log("count", count);
25 if (count > 2) {
26 cleanupFunction();
27 }
28}

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.

1import {useEffect} from "react";
2
3function Component() {
4 useEffect(() => {
5 // litsen on scroll event
6 window.addEventListener("scroll", () => {
7 console.log("scroll");
8 });
9
10 // remove the event listener when the component is unmounted
11 return () => {
12 window.removeEventListener("scroll", () => {
13 console.log("scroll");
14 });
15 };
16 }, []);
17
18 return <div>Component</div>;
19}
1import {useEffect} from "react";
2
3function Component() {
4 useEffect(() => {
5 // litsen on scroll event
6 window.addEventListener("scroll", () => {
7 console.log("scroll");
8 });
9
10 // remove the event listener when the component is unmounted
11 return () => {
12 window.removeEventListener("scroll", () => {
13 console.log("scroll");
14 });
15 };
16 }, []);
17
18 return <div>Component</div>;
19}

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:

1fn main() {
2 let mut count = 0;
3 let mut incremnt = || {
4 count += 1;
5 println!("count is {}", count);
6 };
7 incremnt(); // count is 1
8 incremnt(); // count is 2
9 incremnt(); // count is 3
10 incremnt(); // count is 4
11 println!("{}", count); // 4
12}
1fn main() {
2 let mut count = 0;
3 let mut incremnt = || {
4 count += 1;
5 println!("count is {}", count);
6 };
7 incremnt(); // count is 1
8 incremnt(); // count is 2
9 incremnt(); // count is 3
10 incremnt(); // count is 4
11 println!("{}", count); // 4
12}

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.