Closures: Subtle Yet Powerful

JavaScript has evolved over the years by introducing a lot of new features & taking a lot of notes from other languages as well. It contains so many small concepts that developers use everyday without even noticing. Closures are one of those concepts. You won't notice them until you actually notice them or until there is a bug. So let's dive in & see how closures are responsible for small features that we use in our everyday life.

Closures

Closures are nothing but functions combined with their surrounding state or lexical environment. You can say that closures give you access to the parent or outer function's scope from the child or inner function. Closures are created every time a function is created.

function parent() {
  const hello = "Hello";

  function child() {
    console.log(hello); // Hello
  }

  child();
}

If you take a look at the above code, the variable hello is displayed in the console in the child() function but the variable is not declared in the function. It is declared in the parent() function but the child() function is still able to access the variable as a closure is formed when the child() function is created. Let's inspect things in the console.

If you look into the scope area, instead of Local scope it is showing a Closure at the breakpoint. This is because, child() is declared inside parent() & a closure is formed. It is as simple as that!

Closures are used in a lot of places such as:

  • Currying

  • Module Design Pattern

  • Data Hiding

  • Memoization

  • Maintaining the variables & their values in asynchronous calls, etc.

Deep Dive

Let's take a deep dive & see how closures are present everywhere in our simple day-to-day programs.

function parent() {
  const welcomeString = "Hello";

  return function child(name) {
    return `${welcomeString} ${name}`;
  }

  child();
}

const res = parent()("Danny"); // Hello Danny

// the above line is same as:
// const child = parent();
// const res = child("Danny");

console.log(res);

In the above program we are using function currying to display the output of the function. It is just another way writing function calls where you pass each argument one-by-one rather than passing all together. This is possible only because of closures. You might wonder why. Let's understand it by having a look at normal way (without currying) of writing this program.

// above function call without currying
const child = parent();
const res = child("Danny");

Once the parent() function is finished running, the local environment or the function execution context (FEC) will be deleted & because of this welcomeString will be cleared from the memory as well. Now when we execute child() function, we need the welcomeString variable & the FEC is already cleared from the memory, but the program still works. This is because the child() function will carry the lexical environment of the parent() along with it as a closure is formed. Hence the program runs without any errors or weird console statements.

If you put a debugger at line 5, you can see that along with the Local scope, which contains the argument passed to the child() function, we also have a closure which still holds the value of welcomeString.

Closures in Loop

Note: This example is taken from MDN Documentation of Closures. You can find the live demo here

<!-- this element should be updated -->
<p id="help">Helpful notes will appear here</p>

<p>E-mail: <input type="text" id="email" name="email"></p>
<p>Name: <input type="text" id="name" name="name"></p>
<p>Age: <input type="text" id="age" name="age"></p>
function showHelp(help) {
  document.getElementById("help").textContent = help;
}

function setupHelp() {
  var helpText = [
    { id: "email", help: "Your e-mail address" },
    { id: "name", help: "Your full name" },
    { id: "age", help: "Your age (you must be over 16)" }
  ];

  for (var i = 0; i < helpText.length; i++) {
    var item = helpText[i];
    document.getElementById(item.id).onfocus = function () {
      showHelp(item.help);
    };
  }

  console.log("Finished!");
}

setupHelp();

I've added a line console.log("Finished!") for debugging purpose. If we see the demo, we can see that the helper text is not getting updated & it is set to the value mapped to age. The reasons for this are:

  1. The functions assigned to onfocus are closures.

  2. In total, 3 closures are created.

  3. They all contain the parent environment which is setupHelp().

  4. The value of item.help is calculated only when the event is triggered.

  5. The above value is based on value of item.

This is what they've mentioned in the MDN docs. Now let's take a deeper look & debug the issue here.

We all know that var are hoisted at the Local scope. In this case, it is hoisted at setupHelp(). When the event is triggered, the onfocus tries to evaluate the item.help value based on item. Now as the program is already executed, the last value of item was the helpText[2] & we all know the closures formed at onfocus will have the lexical environment of setupHelp(), so the last stored value is taken from that lexical environment for item. That is why, the string mapped to age is displayed on every event trigger. Have a look at the program's state after the execution of the loop.

We can solve this issue by using let instead of var as it is block scoped, so every closure will have it's own item or you can use a forEach loop as it will have another callback function inside it. There are other solutions mentioned in the MDN Docs as well, make sure to have a look at them.

Performance Consideration

As you can see in the above examples that the closure contains the lexical environment of the parent function as well, this might lead to a lot of memory consumption. If there is a multi-level nesting of functions with separate arguments for each function, the closure will contain the lexical environment of all the parent functions. So, it is unwise to use closures where there is no need as it affects the performance of your application.

Other Examples

It is hard to cover & explain multiple example in a single article. Here is a list of examples for closures:

Summary

  1. Closures are functions along with their surrounding lexical environment.

  2. Closures are used in a lot of places such as currying, module design pattern, data hiding, memoization, maintaining the variables & their values in asynchronous calls, etc.

  3. Using closures unnecessarily can affect the application's performance in terms of both - speed & memory consumption.