In my last post, we talked about scope in JavaScript and how functions can be inside of functions. In this article, we will talk about closures and how it is related to scope.
I have come to realize that some of us JavaScript devs use closures without even realizing it, funny eh?.
How closures work in JavaScript can be quite difficult to grasp, but I will try my best to take it step by step in this article. But first, let's know what closures are.
What is a closure?
A closure allows access(of variables) from an inner function to a parent function. Consider the code below:
We see in the above code, how that function innerFunc
has access to the variable a
which was declared in the outer function func
. We can therefore call function innerFunc
a closure.
Note that outer functions cannot have access to variables in their inner functions.
In line 9 of the code above, we called the function func
and assigned it to a variable inner which makes it a function we can later call in the next line. This was done like this because calling the function directly without assigning it to a variable will only just return the innerFunc function
which is not reusable.
In line 10, we see how that function inner
returns 7.
Now that we have seen a practical example of a closure. Let's get into how closures work in JavaScript.
How do closures work?
Well, for us to really understand how closures work in JavaScript, we have to first understand some of the basics of how JavaScript works(more on that in a future article). There are two important concept in JavaScript that we have to know.
- The Execution Context
- The Lexical Environment
1. The Execution Context
An execution context refers to an environment where JavaScript code is executed. Talk about functions, variables in functions etc. Two codes can be run in the execution context.
The global code: When a global code is executed, it is executed in global context.
The function code: When a function code is executed, it is executed in the function context.
A collection of active execution context forms an execution stack or a call stack.
For example, we'll assume an execution stack as an array before we continue.
EStack = [];
The stack is pushed every time we call a new function.
When you are running a global code, the stack array contains the global code which looks like this:
EStack = [ globalContext ];
When it encounters a function call, the function goes to the top of the stack, the stack then looks like this.
EStack = [
functionContext
globalContext
]
After the function code completes, it gets popped off from the stack. The stack then looks like this.
EStack = [
globalContext
]
The global context is usually at the bottom of the stack.
The Lexical Environment
As the execution context executes the global and function code, it creates a lexical environment where it stores all the variables defined in that function.
The environment record(inner environment) and the reference to the outer environment record are all contained in the lexical environment.
Let's see how the concept of a lexical environment looks like.
lexicalEnvironment = {
environmentRecord: {
<identifier> : <value>, //inner environment record
<identifier> : <value> //inner environment record
}
outer: < Reference to the parent environment > //outer environment record
Now, let's understand the illustration above with a code snippet.
let a = 3;
function func() {
let b = 4;
}
func();
As the code above is run, a global execution context is created for the global code and a function execution context is created for the function code where a lexical environment that stores variables and functions is also created.
Let's tackle what's happening in the global execution context.
globalLexicalEnvironment = {
environmentRecord: {
a : 3,
func : < reference to function object >
}
outer: null
}
We see from the above illustration that the outer environment record is set to null. This is because a global code is the outermost scope of a program. Therefore it has no outer lexical environment.
Like for the global code above, an execution context is also created for the function code. Only that this is a function execution context. The function lexical environment will look like this:
functionLexicalEnvironment = {
environmentRecord: {
b : 4,
}
outer: <globalLexicalEnvironment>
}
We see that the outer environment is set to the global lexical environment because the function is wrapped inside the global scope.
Remember, when we were explaining the environment context above we said that after a function code has been run, its function execution context gets popped off from stack. The case isn't the same for its lexical environment though, as it may or may not get popped off on condition of whether it is being referenced by an inner environment or not. Understanding this plays a big role in how closures work.
Below, we are going to see such condition where it doesn't get popped off.
Consider the code below:
function firstFunc() {
let a = 3;
return function secondFunc() {
let b = 4;
return a + b;
};
}
In the code snippet above, the execution context of the function firstFunc
will be popped off from the stack but its lexical environment won't because it's variable a
is being referenced by it's inner function secondFunc
. Thus, the lexical environment of the firstFunc
and secondFunc
will look like this respectively:
firstFuncLexicalEnvironment = {
environmentRecord: {
a: 3,
SecondFunc : < reference to function object >
}
outer: <globalLexicalEnvironment>
}
secondFuncLexicalEnvironment = {
environmentRecord: {
b : 4,
}
outer: <firstFuncLexicalEnvironment>
}
We see in the secondFunc
lexical environment that the outer environment it references is the firstFunc
function. This is because secondFunc
references a variable a
which is not defined in its scope. Noticing that secondFunc
references a variable that is not defined in it's scope, the JavaScript engine looks into the outer environment which is firstFunc
in this case, and then finds the variable a
. Then 7(a + b)
is returned.
If the lexical environment of the firstFunc
is also popped off as it's execution context, we will get an error that says a is not defined
.
Conclusion
Now that we have come to the end of the article, let's review what we learnt so far.
We learnt about
- Closures,
- How closures work,
- The Execution Context and the lexical Environment.
I hope that from this article, you have been able to get an understanding of how closures work in JavaScript.
If you have any questions, feel free to ask them in the comment section. Until next time, XOXO.
Top comments (0)