Understanding Closures in JavaScript

March 12th, 2020

  1. What's a Closure?
  2. Uses for Closures
  3. How Might This Trip Us Up?

# What's a Closure?

When you declare a function inside another function, a closure is the new environment created by combining the inner function with references to all variables available to it from outer scopes (this concept of all scopes accessible from a certain area is known as the lexical environment).

In other words, in a closure, all variables accessible to the inner function -- including variables declared outside the function itself -- remain accessible to it, even when that inner function is removed and called in some other context. The inner function remembers all the stuff it has access to at the time of its declaration.

Let's look at an example:

1let makeSayFullNameFunction = () => {
2 let lastName = `Skywalker`;
3 return firstName => {
4 return `${firstName} ${lastName}`;
5 };
6};
7
8let sayFullName = makeSayFullNameFunction();
9sayFullName(`Luke`); // Luke Skywalker

Here, lastName is locally scoped to makeSayFullNameFunction. So it might seem that when we pull out the returned function as sayFullName and call it, we'll get an error, because it relies internally on lastName, but lastName isn't accessible from the global scope.

But in fact, this works just fine. When the inner function is created, lastName is enclosed (or closed over) into the closure of the inner function, so it is considered in scope no matter where the function is called.

For the purposes of calling the inner function, this:

1let makeSayFullNameFunction = () => {
2 let lastName = `Skywalker`;
3 return firstName => {
4 return `${firstName} ${lastName}`;
5 };
6};

...is equivalent to this:

1let makeSayFullNameFunction = () => {
2 return firstName => {
3 let lastName = `Skywalker`;
4 return `${firstName} ${lastName}`;
5 };
6};

The main benefit of closures is that they allow us to compose more modular programs. We don't have to stuff everything a function needs into that function to ensure it'll be able to access everything it needs in another environment, as we're about to see.

# Uses for Closures

1. When a Function Returns a Function

Let's look at our example from above again:

1let makeSayFullNameFunction = () => {
2 let lastName = `Skywalker`;
3 return firstName => {
4 return `${firstName} ${lastName}`;
5 };
6};
7
8let sayFullName = makeSayFullNameFunction();
9sayFullName(`Luke`); // Luke Skywalker

Even though lastName doesn't appear to be in scope when sayFullName is called, it was in scope when the function was declared, and so a reference to it was enclosed in the function's closure. This allows us to reference it even when we use the function elsewhere, so that it's not necessary to stuff everything we need in scope into the actual function declaration.

2. When a Module Exports a Function

1// sayName.js
2
3let name = `Matt`;
4
5let sayName = () => {
6 console.log(name);
7};
8
9export sayName;
1// index.js
2
3import sayName from '/sayName.js';
4
5sayName(); // Matt

Again, we see that even though name doesn't appear to be in scope when sayName is called, it was in scope when the function was declared, and so a reference to it was enclosed in the function's closure. This allows us to reference it even when we use the function elsewhere.

3. Private Variables and Functions

Closures also allow us to create methods that reference internal variables that are otherwise inaccessible outside those methods.

Consider this example:

1let Dog = function() {
2 // this variable is private to the function
3 let happiness = 0;
4
5 // this inner function is private to the function
6 let increaseHappiness = () => {
7 happiness++;
8 };
9
10 this.pet = () => {
11 increaseHappiness();
12 };
13
14 this.tailIsWagging = () => {
15 return happiness > 2;
16 };
17};
18
19let spot = new Dog();
20spot.tailIsWagging(); // false
21spot.pet();
22spot.pet();
23spot.pet();
24spot.tailIsWagging(); // true

This pattern is only possible because references to happiness and increaseHappiness are preserved in a closure when we instantiate this.pet and this.tailIsWagging.

# How Might This Trip Us Up?

One big caveat is that we have to remember we're only enclosing the references to variables, not their values. So if we reassign a variable after enclosing it in a function...

1let name = `Steve`;
2
3let sayHiSteve = () => {
4 console.log(`Hi, ${name}!`);
5};
6
7// ...many lines later...
8
9name = `Jen`;
10
11// ...many lines later...
12
13sayHiSteve(); // Hi, Jen!

...we might be left with an unwanted result.

In ES5, this often tripped up developers when writing for loops due to the behavior of var, which was then the only way to declare a variable. Consider this situation where we want to create a group of functions:

1var sayNumberFunctions = [];
2
3for (var i = 0; i < 3; i++) {
4 sayNumberFunctions[i] = () => console.log(i);
5}
6
7sayNumberFunctions[0](); // Expected: 0, Actual: 3
8sayNumberFunctions[1](); // Expected: 1, Actual: 3
9sayNumberFunctions[2](); // Expected: 2, Actual: 3

Though our intention is to enclose the value of i inside each created function, we are really enclosing a reference to the variable i. After the loop completed, i's value was 3, and so each function call from then on will always log 3.

This bug arises because var (unlike let) can be redeclared in the same scope (var a = 1; var a = 2; is valid outside strict mode) and because var is scoped to the nearest function, not the nearest block, unlike let. So each iteration was just changing the value of a single global-scope variable i, rather than declaring a new variable, and that single variable was being passed to all of the created functions.

The easiest way to solve this is to replace var with let, which is block-scoped to each iteration's version of the loop block. Every time the loop iterates, i declared with let will be a new, independent variable scoped to that loop only.

1var sayNumberFunctions = [];
2
3for (let i = 0; i < 3; i++) {
4 sayNumberFunctions[i] = () => console.log(i);
5}
6
7sayNumberFunctions[0](); // 0
8sayNumberFunctions[1](); // 1
9sayNumberFunctions[2](); // 2

But what if for some reason we can't use let? Alternatively, we could work around this problem by changing what's being enclosed:

1var sayNumberFunctions = [];
2
3for (var i = 0; i < 3; i++) {
4 let newFunction;
5
6 (function(iInner) {
7 newFunction = () => console.log(iInner);
8 })(i);
9
10 sayNumberFunctions[i] = newFunction;
11}
12
13sayNumberFunctions[0](); // 0
14sayNumberFunctions[1](); // 1
15sayNumberFunctions[2](); // 2

We can't use let, so we have to find a new way to enclose a unique value into newFunction. Since var is function-scoped, we'll need to declare another function and then immediately invoke it. Since we're declaring and invoking a new function on each iteration, our variable iInner is being redeclared as a unique variable each time, so we're now enclosing a unique variable with its own unique value on each pass, preserving the value we want.

As you've probably noticed, forcing the developer to use closures to detangle local variables from the global state is less than ideal. This was a major impetus for the behavior of let in ES6.

But it's still a good idea to understand how closures work, and to keep in mind that they don't freeze the lexical environment's values; they only preserve references to variables that are in scope.