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 offoo
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.
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 ofouter
function) - local environment of the
inner
function (created on every invocation ofinner
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:
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.