Functions in JavaScript- Closures
We spoke all about arguments and parameters in the previous article. Let us now explore a concept that many find confusing the first time they hear about it- closures.
What is a Closure? The definition that you won't understand
"A closure is an expression that has free variables together with an environment that binds those variables". Now, that statement hardly makes sense to anyone that isn't a pro developer. It's the kind of thing that one would spend little effort trying to understand, unless they were forced to do so.
All I understood about closures the first time I read about them was that a nested function and its containing function somehow formed something called a 'closure' and that was that. Over time, it bothered me enough to dig deeper and learn what really was going on under the hood.
Execution Contexts, Lexical Environments and Lexical Environment Chains
To understand how closures really work, we need to familiarize ourselves with a couple of terms first- execution contexts and lexical environments.
An "Execution Context Stack" or the "call-stack" is a JavaScript construct that keeps track of the code that is being run. It may be visualized as a stack, with the global execution context at the bottom. When the execution of a new function begins, a new execution context is pushed onto the top of the call stack and when the function's execution is completed, it's execution context is popped off from the top of the stack.
The "Lexical Environment" is another JavaScript construct that keeps track of the "identifier-variable" mapping- i.e it keeps track of the variables in an execution context and the objects or values that they point to. Any functions that are declared in the current execution context are also pointed to in the lexical environment. In addition to the function code, function objects also contain certain additional properties. [[scope]] is one such internal property. This property refers to the current scope- i.e. the scope in which the function is defined. Thus, the function contains a reference to the lexical environment in which it is defined.
It will soon become apparent why these details are important when we discuss how closures actually work.
Consider the following-
// test.js
function add(a, b){
return a+b;
}
let a = 10;
let b = 20;
let c = add(a,b);
// 30
Before function is pushed onto the call stack
Before reaching the line with let c = add(a,b)
, the call-stack and lexical environments look like this-
On reaching the line let c = add(a,b)
, the add
function is invoked. As a result, the function is pushed onto the call-stack and a new lexical environment is created for the newly created execution context. This results in the formation of a lexical environment chain. The new lexical environment contains the variables local to the add
function and also the arguments passed to it. When we try to access a variable from within the add
function, JS tries to find this variable in the current lexical environment (add
's lexical environment). If this search fails, JS travels up the lexical environment chain and tries to find the variable in the parent lexical environment (the global lexical environment). If the variable isn't found in any of the lexical environments, a ReferenceError
is thrown.
After the function has been pushed onto the stack
Here is the scenario during add()
function execution-
We can see 'data-hiding' in practice in the above image. When we try to access a
or b
during the execution of the add()
function, we are unable to access the global a
or b
as add()
's a
and b
hide them- i.e JS finds a
in add()
's lexical scope and as a result doesn't travel up the lexical environment chain to arrive at the global a
or b
After the function has been popped off the stack
Once the add()
function's execution is completed, its execution context is popped off the call-stack. Since add()
's lexical environment has no more references to it, it is garbage collected.
Here's a look at the scenario after add()
has been popped off-
What is a closure? The explanation that actually makes sense
Before we go into the technicalities of closures, let's take a look at an example-
function multiplyGenerator(x){
return function(n){
return n*x;
}
}
// the above forms a closure
times5 = multiplyGenerator(5);
a = times5(2);
// a contains the value 10
Here, the multiplyGenerator()
function and the anonymous function returned by it form a closure. We can observe here, that the times5
function is still able to access the x
argument given to the enclosing multiplyGenerator
function, even though that function has already returned. This could be puzzling because, we saw earlier that a function's lexical scope is garbage collected when it returns. However, this is not the case for closures, because the returned function still maintains a reference to the enclosing function.
Let's take a look at the states of the call-stack and the lexical environments at various stages of program execution to develop a better understanding.
Before multiplyGenerator()
is invoked
We can see that the call-stack has the global execution context which points to its lexical scope. The multiplyGenerator
identifier has a reference to the scope it's defined in (the global lexical environment).
During multiplyGenerator()
execution
We can see that multiplyGenerator()
's execution context is pushed onto the stack and it references it own lexical environment. This forms a lexical environment chain with the global execution context's lexical environment.
When multiplyGenerator() returns
Even though multiplyGenerator()
has returned, it's lexical environment isn't garbage collected. This is because the returned function (stored in times5
) maintains a reference to the lexical environment that it is defined in. This is why times5
is able to access the x
argument passed to the multiplyGenerator()
function at a later point. At this point, the variable x
is inaccessible as there is no execution context that references the multiplyGenerator()
lexical environment and no other lexical environment inherits it.
During inner function execution
During the execution of the times5
function, its execution context is pushed onto the call stack and a corresponding lexical environment is created for it. The newly created lexical environment inherits from the multiplyGenerator()
lexical environment, growing the depth of the lexical environment chain. We can note here that the variable x
in the multiplyGenerator()
lexical environment is only accessible from the times5
function. The x
variable cannot be accessed by any other means- acting as a private data member. In fact, closures are most commonly used for achieving this functionality- creating private data members.
After inner function execution
This figure showed us the scenario after the execution of the inner function. times5
's execution context is popped off of the stack and its lexical environment is garbage-collected as they are no more references to it.
The definition that you won't understand, revisited (Free variables)
"A closure is an expression that has free variables together with an environment that binds those variables". Remeber this? There is one last thing left for us to address- what is a free variable? A free variable is nothing but a variable used within the function, that is neither an argument nor a local variable. x
was the free variable in our example as it wasn't local to the anonymous function, nor was it an argument passed to the function. The environment that bound the variable was the lexical environment chain.
Awesome! Now, you have the opportunity to not help somebody by saying "A closure is an expression that has free variables together with an environment that binds those variables".
Closing statements
That was all about how closures work. I hope you have gotten a good understanding of closures and how they work.
This article wraps up the JavaScript functions series. Hope you lot got a solid base to continue on with your development journey from here. Happy coding!
Subscribe to my newsletter
Read articles from Senthooran B directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by