In this article I will try to disassemble the mechanism for implementing closures in JavaScript. For this, I will use the Chrome browser.
Let's start with the definition:
Closures are functions that refer to independent (free) variables. In other words, the function defined in the closure “remembers” the environment in which it was created.
MDNIf something is not clear to you in this definition, it is not scary. Just read on.
')
I am deeply convinced that to understand something easier and faster with concrete examples.
Therefore, I propose to take a code snippet and walk along it along with the interpreter from beginning to end, in steps and understand what is happening.
So let's get started:
Picture 1We are in the global context of the call, it is Global (also known as the Window in the browser) and we see that the main function already lies in the current context and is ready to work.
Figure 2This happens because all Function Declaration (hereinafter FD) always go up in any context, they are immediately initialized and ready to go. The same thing happens with variables declared via var, only their values are initialized as undefined.
It is also important to understand that JavaScript also “raises” variables declared via let and const. The only difference is that it does not initialize them as var or as FD. Therefore, when we try to access them before initialization, we get a Reference Error.
Also in main, we see the internally hidden property
[[Scopes]] - this is a list of external contexts to which main has access. In our case, Global is there, since main is running in a global context.
The fact that in JavaScript initialization of references to the external environment occurs at the moment of creating the function, and not at the moment of execution, says that JS is a language with a static scope. And this is important.
Go ahead:
Figure 3We go into the main function and the first thing that catches the eye is the Local object (in the specification, localEnv). There we see
a , since this variable is declared via
var and it 'floated up', well, and by tradition we see all 3 FD (foo, bar, baz). Now let's see where it all came from.
When you run any context, the abstract operation
NewDeclarativeEnvironment is started , which allows you to initialize
LexicalEnvironment (hereafter LE) and
VariableEnvironment . Also,
NewDeclarativeEnvironment takes 1 argument — an external LE, in order to create the [[Scopes]] that we talked about above. LE is an API that allows us to define the relationship between identifiers and individual variables, functions. LE consists of 2 components:
- Record Environment - a recording of the environment, which allows to determine the relationship between identifiers and what is available to us in the current context of the call
- Link to external LE. Each function has an internal property [[Scopes]] at creation
VariableEnvironment - most often it is the same as LE. The difference between them is that the value of the VariableEnvironment never changes, and the LE can change during the execution of the code. To simplify further understanding, I propose combining these components into one - LE.
Also in the current Local, there is this due to the fact that the
ThisBinding call
occurred - this is also an abstract method that initializes this in the current context.
Of course, each FD immediately received [[Scopes]]:
Figure 4We see that all FD received in [[Scopes]] an array of [Closure main, Global], which is logical.
Also in the picture we see
Call Stack - this is a data structure that works on the principle of LIFO - last in first out. Since JavaScript is single-threaded, only one context can be executed at a time. In our case, this is the context of the main function. Each new function call creates a new context, which is added to the stack.
At the top of the stack is always the current execution context. After the function has completed its execution and the interpreter has left it, the context of the call is removed from the stack. That's all we need to know about the call stack in this article :)
We summarize what happened in the current context:
- During creation, main got [[Scopes]] with links to the external environment.
- The interpreter entered the main function body
- The call stack has the execution context main
- This has been initialized
- LE initialization occurred
In fact, the hard part is over. Go to the next step in the code:
Now we need to call baz to get the result.
Figure 5A new baz call context has been added to the Call Stack. We see that a new Closure object has appeared. Here comes what is available to us from [[Scopes]]. So we got to the point. This is the closure. As you can see in
Figure 4, Closure (main) goes first on the baz list of 'backup' contexts. Again, no magic.
Let's call foo:
Figure 6It is important to know that no matter what place we call foo, it will always go after its unidentified identifiers in its [[Scopes]] chain. Namely, in main and then in Global, if not found in main.
After executing foo, it returned a value, and its context was dropped from the Call Stack.
Go to the function call bar. In the execution context, bar has a variable with the same name as the variable in LE foo -
a . But, as you may have guessed, it does not affect anything. foo will still take the value from its [[Scopes]].
Figure 7As a result, baz will return 300 and will be thrown out of the call stack. Then the same will happen with the context context, our code fragment will finish executing.
We summarize:
- During the creation of the function is set [[Scopes]] . This is very important for understanding closures, because when searching for values, the interpreter immediately follows these links.
- Then, when this function is called, an active execution context is created, which is placed in the Call Stack.
- ThisBinding is performed and this is set for the current context.
- LE initialization is performed, and all function arguments, variables declared through var and FD become available. Further, if there are variables declared via let or const, they are also added to LE
- If the interpreter does not find any identifier in the current context, then [[Scopes]] is used for further search, which are sorted out, in turn. If the value is found, then the link to it falls into a special Closure object. At the same time, for each context that the current closes to, a separate Closure is created with the necessary variables.
- If no value is found in any Scopes, including Global, the ReferenceError is returned.
That's all!
I hope this article was useful to you and now you understand how the closure mechanism works in JavaScript.
Bye everyone :) And until we meet again. Like and subscribe to my channel :)