JavaScript Execution Mechanisms (3) Scope Chains and Closures
In上一篇关于作用域的文章We talk about scope through the call stack.
Whenever we call a function, we create an execution context for that function in the call stack. So far we know that there is a variable environment for storing variables declared through var, and a lexical environment. The lexical environment, in turn, is a stack that places the variables in the function declared through let and const in different levels of block-level scope **.
Based on the above variable environment and lexical environment, we can understand how to find the correct variable in the execution context of a function.
What if the variable we need is not actually in the current execution context, but in the execution context at the previous level? How do we determine which execution context is at the previous level?
This question touches on our topic today: scope chains.
Understanding scope chain is the basis for understanding closures, which are almost ubiquitous in JavaScript. At the same time, scope and scope chain are the foundation of all programming languages. Therefore, if you want to learn a language thoroughly, scope and scope chain must be inseparable.
Scope chain
First, let’s take a look at the following code:
1 | function bar() { |
What do you think the bar function and foo function in this code print out?
So when this code executes inside the bar function, the state diagram of its call stack is as follows:
As can be seen from the figure, both the global execution context and the execution context of foo function contain the variable myName. Which one should be selected for the value of myName in the bar function?
Perhaps your first reaction is to search for variables in the order of the call stack, as follows:
- First find if there is a myName variable in the stack top, but there is none here, so then go down and look for the variable in the foo function.
- The myName variable is found in the foo function, and myName in the foo function is used at this time.
If you look for variables in this way, the final result printed by executing the bar function should be “big tree”. But this is not the case. If you try to execute the above code, you will find that the printed result is “bigTree”. Why is this the case? To explain this problem, then you need to figure out the scope chain first.
Outer pointer
In fact, in the variable environment of each execution context, there is an external reference used to point to the external execution context. We call this external reference outer.
When a piece of code uses a variable, the JavaScript engine will first look for the variable in the “current execution context”.
For example, when the code above searches for the myName variable, if it is not found in the current variable environment, the JavaScript engine will continue to search in the execution context pointed to by outer. For an intuitive understanding, you can see the following picture:
It can be seen from the figure that the outer of bar function and foo function both point to the global context, which means that if an external variable is used in bar function or foo function, the JavaScript engine will look for it in the global execution context.
We call this search chain the scope chain. Now you know that variables are searched through the scope chain, but there is still a question that has not been solved. The bar function called by foo function, why is the external reference of bar function the global execution context instead of the execution context of foo function?
To answer this question, you also need to know what lexical scope is. This is because during JavaScript execution, its scope chain is determined by lexical scope.
Lexical scope means that the scope is determined by the position of the function declaration in the code, so the lexical scope is a static scope, through which it is possible to predict how the code will look up identifiers during execution.
In other words, lexical scope is determined during the compile stage of the code, and has nothing to do with how the function is called.
Variable lookup in block-level scope
Previously, we analyzed the scope chain through the global scope and function-level scope. Next, let’s take a look at how variables are found in the block-level scope? When writing code, if you use a variable that does not exist in the current scope, the JavaScript engine needs to search for the variable in other scopes according to the scope chain. If you do not understand the process, there will be a high probability of writing unstable code.
1 |
|
You can first analyze the execution process of this code by yourself to see if you can analyze the execution result. To get the execution result, then we have to analyze the execution process from the perspective of the scope chain and the lexical environment.
ES6 supports block-level scope. When executing to the Code Block, if there is a variable declared by let or const in the Code Block, the variable will be stored in the lexical environment of the function. For the above code, when executing to the if statement block inside the bar function, the call stack is shown in the following figure:
Closure
First, let’s talk about what closure is.
The first and most essential statement is that a closure is a function with its own runtime environment, that is, the function has its own execution context.
The other is a popular, more representational explanation in terms of usage: first, it is closed, that is, the outside world cannot directly access the variables in it, and second, it is a package, so it exposes Some methods allow the outside world to manipulate its internal data.
Here you can combine the following code to understand what a closure is:
1 |
|
First, let’s take a look at the stack when executing the return innerBar line of code inside the foo function. You can refer to the following figure:
As you can see from the above code, innerBar is an object that contains two methods of getName and setName (usually we call the function inside the object a method). You can see that both methods are defined inside foo function, and both methods use two variables, myName and test1.
** According to the rules of lexical scope, the inner function getName and setName can always access the variables in their outer function foo **, so when the innerBar object returns to the global variable bar, although the foo function has finished executing, getName and setName function can still use the variables myName and test1 in the foo function. So when the foo function is executed, the state of its entire call stack is shown in the following figure:
As can be seen from the above figure, after the execution of foo function is completed, its execution context pops up from the stack top, but because the returned setName and getName methods use the variables myName and test1 inside foo function, these two variables are still saved in memory. This is very much like a dedicated backpack behind the setName and getName methods. No matter where the setName and getName methods are called, they will carry the exclusive backpack of this foo function.
The reason why it is a dedicated backpack is that the backpack cannot be accessed anywhere except for the setName and getName functions. We can call this backpack the closure of the foo function.
Okay, now we can finally give a formal definition of closure. ** In JavaScript, according to the rules of lexical scope, the inner function can always access the variables declared in its outer function. When an inner function is returned by calling an outer function, even if the outer function has finished executing, but the variables that the inner function refers to the outer function are still stored in memory, we call the collection of these variables closure. For example, if the outer function is foo, then the set of variables is called the closure ** of the foo function.
Closure recycling
After understanding what a closure is, let’s briefly talk about when a closure is destroyed. Because if the closure is used incorrectly, it can easily cause memory leaks. Paying attention to how the closure is reclaimed can make you use the closure correctly.
Usually, if the function referencing the closure is a global variable, the closure will exist until the page is closed; but if the closure is no longer used in the future, it will cause a memory leak.
If the function that references closure is a local variable, after the function is destroyed, the next time the JavaScript engine executes garbage collection, if the closure is no longer used, the JavaScript engine’s garbage collector will reclaim this memory.
So when using closures, you should try to pay attention to a principle: if the closure will be used all the time, then it can exist as a global variable; but if the frequency of use is not high, and the memory footprint is relatively large, then try to make it a local variable.