JavaScript closures are tough to wrap your head around the first time you encounter them. Some developers might come off forming a wrong mental model about closures as it is very easy to get closures in the wrong way.
Perhaps reading the code that uses closure in a linear fashion can be an easily misleading way to form a wrong mental model about it. In this post I am going to disclose what closures actually are.
Let's start by understanding how the JavaScript engine parses our code.
How JavaScript Engine Works
It goes by the code line by line.
Any function declaration and variables it finds is put in the global memory.
(Putting these functions and variable in the global memory is called hoisting.)
// Values like below are put in the global memory.
const someVariable = 123
function myFirstFunc() {
console.log('This is my awesome function')
}
const mySecondFunc = function mySecondFunc() {
console.log('This is my awesome function')
}
At this point the JavaScript code is compiled, and the engine will again go line by line.
When the engine hits a function it checks its global memory for the function and creates a temporary environment for that function which is known as its execution context.
The fact that the function is pulled out from the global memory is worth emphasizing which you'll learn soon why.
The execution context has 2 parts - a memory and a place to execute the statements inside the function. This execution context is unique to the function.
The function is also added at the top of call stack, the global()
always rests at the bottom of this call stack. The call stack basically tells the engine what to work on, so, the function on the top of JavaScript is what the engine will work on.
All the arguments passed in the function are evaluated (if you pass in a variable
a
as an argument which was assigned a value of1
, thena
is changed to1
),These evaluated arguments are added to the memory part of the execution context of the function. In the memory these arguments are saved by the labels given according to the parameters of the function.
function myElegantFunction(myParameterOne, myParameterTwo) {
console.log(myParameterOne, myParameterTwo)
}
myVariableOne = 'Hello'
myVariableTwo = 'World'
myElegantFunction(myVariableOne, myVariableTwo)
/** myElegantFunction(myVariableOne, myVariableTwo)
is changed to
myElegantFunction('hello', 'world')
Let's see the memory part of the execution context of myElegantFunction,
----------
myParameterOne: 'Hello'
myParameterTwo: 'World'
----------
As you can see how these arguments are saved according to the name of the parameter which we referenced in the function declaration.
**/
Now the statements inside of the function are executed one by one, if it contains any variable it is first looked in the memory part of the execution context of that function if the variable is not found then the engine tried to search for it in the global scope.
The function is removed off the call stack and the
global()
proceeds to run the JavaScript code.
To give more clarity I have made a small video animation visually explaining this process exclusively for this post.
By now you must have understood how the call stack, execution context, and memory work all together to achieve the task of running your code. Keeping the above procedures in mind, this is the perfect time to introduce you to closures.
Getting close to closures
Let's consider a function -
function counterFunction() {
let counter = 0;
function increaseCounter() {
counter++;
console.log(counter);
}
return increaseCounter;
}
The function counter
is a higher-order function as it returns another function namely increaseCounter
.
Let's declare assign this function to a variable.
const count = counterFunction();
When JavaScript is executing the above line, it puts the function increaseCounter
in its global memory
. So what goes in the global memory with the label count is -
count: function increaseCounter() {
counter++;
console.log(counter);
}
// NOTE: Use of ':' (colon) is representational.
Here's where things start getting interesting when we call count
count(); // Output is 1
count(); // Output is 2
count(); // Output is 3
JavaScript is in fact, getting the function from the global memory,
function increaseCounter() {
counter++;
console.log(counter);
}
Here's another animated video for the execution of the above code.
As the execution context starts executing the statement, it encounters the variable counter
, the first place it checks is the memory of the execution context itself and the next thing it should check is the global memory.
Anyone familiar with the working of the JavaScript engine should think it is impossible to get variable counter
.
This is where closures come into the play. Let's go back to where we stored counterFunction()
.
const count = counterFunction();
When the increaseCounter
is stored in the count
variable. The count variable literally carries with it the variables from the function counterFunction
, which is the function increaseCounter
was *return*ed from.
In this state it is said that - increaseCounter
has a closure over counterFunction
.
The value of counter
is coming from the closure
which increaseCounter
carried. Every time we call counter++
we don't touch the counter in the counterFunction
we update the counter
variable in the closure of increaseCounter
.
To demonstrate the fact that counter
being updated is not the part of counterFunction()
here's a neat trick.
const count = counterFunction()
const countTwo = counterFunction()
count() // Output is 1
count() // Output is 2
count() // Output is 3
countTwo() // Output is 1
count() // Output is 4
countTwo() // Output is 2
If counter
was being updated from the counterFunction()
instead of the closures of the function count
and countTwo
then the output of countTwo()
must have added on to the value updated previously by the count()
function. But it does not happens.
Conclusion
I mentioned earlier how easy is it to develop a wrong mental model about closures, it is because we tend to read code linearly and tend to confuse lexical scoping to closures, they are similar but not the same.
Closures are a part of the scope of a function. You can be more clear about closures if you use the JavaScript debugger in your browser's developer tool to inspect where the variables are stored.
Chrome literally shows the closure to be a part of that function's scope which they are. Closures are not a link between two functions.
Top comments (2)
Amazing article.
Good job, now I know the behind the scenes of JavaScript.
Let me know what should I write on next :)