How Closures Work in Javascript?

How Closures Work in Javascript?

Closures are a powerful concept and mastering them is essential to understanding the javascript language. Having said that, closures are one of the most confusing concept in javascript and it can take some time and experience to understand how closures really work.

In Javascript, closure is a combination of two things:

  • Function itself
  • Reference to the surrounding environment in which the function is created

Closure allows the access of inner function to outer function's scope, even after the outer function's execution has ended.

Closure is created every time a javascript function is created. As most functions are invoked from the same scope they were defined in, closures go unnoticed. But closures become noticeable when a function is invoked from a different scope than the one it was created in.

Let's look at a simple example of a closure:

function outer() {
  let name = "John";

  function inner() {
    console.log(name);
  }
  return inner;
}

const innerFn = outer();
innerFn();      // John

Output of the above code example might seem counter-intuitive and you might be wondering: How does inner function have access to name variable after outer function has finished executing?

Reason why the above code works is because javascript functions form closures whenever they are created.

In some programming languages, function's locally-defined variables only exist for the duration of that function's execution. When a function's execution ends, variables defined in its local scope are destroyed.

But that's not the case in javascript as is evident from the code example above. So, how do closures really work?

Lexical Scope

To understand closures, we must first understand the rules around the lexical scope.

Consider the following code example:

let name = "global name";

function foo() {
  console.log(name);
}

foo();     /* global name */

When the foo function is called, to print the value of the name variable, javascript needs to know where this identifier is defined.

It first looks for the identifier name in the local scope of the function foo. As there is no such identifier in the local scope of function foo, javascript looks for the name identifier in the surrounding environment of the foo function which, in this case, is the global environment. As the name identifier is defined in the global environment, javascript prints its value on the console.

Let's modify the above code example:

let name = "global name";

function outer() {
  function inner() {
    console.log(name);
  }
  inner();
}

outer();     /* global name */

Now, we have a nested function inner that logs the value of the name variable.

Javascript looks for the name identifier in the same way as it did in the previous code example. It will first look in the local scope of the inner function. If it doesn't finds the declaration in the local scope, it will look in the outer environment of the inner function which, in this case, is the local scope of the outer function. As name identifier is not defined in outer function, javascript will look in the outer environment of the outer function which, in this case, is the global environment.

This process of looking for the declaration of the identifier from the current environment to the outer environment continues until the identifier's declaration is found or javascript, while looking for the declaration, has reached the global environment and the global environment also doesn't contains the declaration. At this point, javascript will do one of the following two things:

  • throw an error if the code is in strict mode
  • declare a global variable for you in non-strict mode

I hope that it is clear to you now how the function's variables are resolved. We can now move on to understanding the inner workings of closures.

Scope Chain

As mentioned above, when javascript can't find an identifier in the local scope of a function, it looks for that identifier in the outer environment of that function. But how does javascript moves from an inner environment to the outer environment?

Answer is the scope chain. To handle nested scopes, different environments are linked to each other: each environment has a link to the one "outside" it, forming a chain which javascript can follow to move from the current environment to its outer environment.

Let's understand this using one of the previous examples:

let name = "global name";

function foo() {
  console.log(name);
}

foo();     /* global name */

In the above code example, there are two environments involved:

  • Global environment
  • Local environment of the foo function (created on every invocation of foo function)

The global environment contains two identifiers: name and foo. The local environment doesn't contains any declarations.

When the foo function is created, javascript saves the link to the global environment (let's call it EnvGlobal) in an internal slot of the foo function object which the ecmascript specification calls the [[Environment]] slot of the function objects. This [[Environment]] slot is used by the javascript to link the current environment to its outer environment.

When the foo function is called, new environment is created for that function call (let's call it EnvFoo). When javascript can't find the name identifier in the local scope (EnvFoo) of the foo function, it follows the link to the outer environment that is saved in the [[Environment]] slot of the foo function object which, in this case, is the global environment (EnvGlobal). This is how javascript will find the name identifier in the above code example and will log the value of the name variable.

Following diagram is a visualization of how global environment and local environment of the foo function, in the above code example, are linked together.

Screenshot 2020-12-20 at 5.32.46 PM.png

Lets take a look at another example to solidify our understanding of scope chain:

function outer() {
  let a = 100;

  function inner() {
    console.log(a);
  }

  return inner;
}

const innerFn = outer();
innerFn();     // 100

In the above code example, there are three environments involved:

  • Global environment
  • local environment of the outer function (created on every invocation of outer function)
  • local environment of the inner function (created on every invocation of inner function)

When outer function is created, javascript will save the reference to the global environment in the [[Environment]] slot of the outer function.

After that, when the outer function is invoked, a new environment (let's call it EnvOuter) is created. This EnvOuter environment is linked to the environment that is saved in the [[Environment]] slot on the outer function which, in this case, is the global environment.

Continuing with the execution of the outer function, when the inner function is created, javascript will save the link to the local environment of outer function, i.e. EnvOuter, in the internal [[Environment]] slot of the inner function.

After the execution of outer function has ended, when the inner function is called, a new environment is created (let's call it EnvInner) for this function call. This environment (EnvInner) is then linked to the environment that is saved in the internal [[Environment]] slot of the inner function which, in this case, is the EnvFoo environment.

This linkage between the three environments, involved in our code example, can be visualized in the following diagram:

Screenshot 2020-12-20 at 6.42.49 PM.png

Above diagram can help us understand how inner function is able to access the local variable in outer function even after the outer function has executed. When javascript can't find the identifier a in the local scope of the inner function, it follows the link to the outer environment and looks for that identifier in the outer environment. As the outer environment, i.e. EnvOuter contains the identifier a, inner function is able to print the value of variable a.

This linkage between different environments is called scope chain and this scope chain is what enables closures in javascript.

It is because of this scope chain that a nested function is able to access the variables defined in the outer function, even after the execution of the outer function has ended. This outer environment is kept in memory as long as the inner function has a reference to it.

Summary

Every time a javascript function is created, a closure is formed and it allows that function to access the scope chain that was in effect when that function was defined.

Each time a function is created, javascript saves the reference to the surrounding environment of the function in the internal [[Environment]] slot on the function object. When that function is called, a new environment is created for that function call and javascript links this new environment to the environment that is saved in the [[Environment]] slot of the function.

Did you find this article valuable?

Support Yousaf Khan by becoming a sponsor. Any amount is appreciated!