DEV Community

Cover image for Understanding Closures in JavaScript
Matt Popovich
Matt Popovich

Posted on • Edited on • Originally published at popovich.io

Understanding Closures in JavaScript

  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:

let makeSayFullNameFunction = () => {
  let lastName = `Skywalker`;
  return (firstName) => {
    return `${firstName} ${lastName}`;
  };
};

let sayFullName = makeSayFullNameFunction();
sayFullName(`Luke`); // Luke Skywalker
Enter fullscreen mode Exit fullscreen mode

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:

let makeSayFullNameFunction = () => {
  let lastName = `Skywalker`;
  return (firstName) => {
    return `${firstName} ${lastName}`;
  };
};
Enter fullscreen mode Exit fullscreen mode

...is equivalent to this:

let makeSayFullNameFunction = () => {
  return (firstName) => {
    let lastName = `Skywalker`;
    return `${firstName} ${lastName}`;
  };
};
Enter fullscreen mode Exit fullscreen mode

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:

let makeSayFullNameFunction = () => {
  let lastName = `Skywalker`;
  return (firstName) => {
    return `${firstName} ${lastName}`;
  };
};

let sayFullName = makeSayFullNameFunction();
sayFullName(`Luke`); // Luke Skywalker
Enter fullscreen mode Exit fullscreen mode

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 expression.

2. When a Module Exports a Function

// sayName.js

let name = `Matt`;

let sayName = () => {
  console.log(name);
};

export sayName;
Enter fullscreen mode Exit fullscreen mode
// index.js

import sayName from '/sayName.js';

sayName(); // Matt
Enter fullscreen mode Exit fullscreen mode

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:

let Dog = function () {
  // this variable is private to the function
  let happiness = 0;

  // this inner function is private to the function
  let increaseHappiness = () => {
    happiness++;
  };

  this.pet = () => {
    increaseHappiness();
  };

  this.tailIsWagging = () => {
    return happiness > 2;
  };
};

let spot = new Dog();
spot.tailIsWagging(); // false
spot.pet();
spot.pet();
spot.pet();
spot.tailIsWagging(); // true
Enter fullscreen mode Exit fullscreen mode

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...

let name = `Steve`;

let sayHiSteve = () => {
  console.log(`Hi, ${name}!`);
};

// ...many lines later...

name = `Jen`;

// ...many lines later...

sayHiSteve(); // Hi, Jen!
Enter fullscreen mode Exit fullscreen mode

...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:

var sayNumberFunctions = [];

for (var i = 0; i < 3; i++) {
  sayNumberFunctions[i] = () => console.log(i);
}

sayNumberFunctions[0](); // Expected: 0, Actual: 3
sayNumberFunctions[1](); // Expected: 1, Actual: 3
sayNumberFunctions[2](); // Expected: 2, Actual: 3
Enter fullscreen mode Exit fullscreen mode

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.

var sayNumberFunctions = [];

for (let i = 0; i < 3; i++) {
  sayNumberFunctions[i] = () => console.log(i);
}

sayNumberFunctions[0](); // 0
sayNumberFunctions[1](); // 1
sayNumberFunctions[2](); // 2
Enter fullscreen mode Exit fullscreen mode

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

var sayNumberFunctions = [];

for (var i = 0; i < 3; i++) {
  let newFunction;

  (function(iInner){
    newFunction = () => console.log(iInner);
  })(i);

  sayNumberFunctions[i] = newFunction;
}

sayNumberFunctions[0](); // 0
sayNumberFunctions[1](); // 1
sayNumberFunctions[2](); // 2
Enter fullscreen mode Exit fullscreen mode

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 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.

Top comments (0)