JavaScript Execution Mechanism (2) Scope

In上一篇博客We talked about variable hoisting in javascript from the perspective of execution mechanism and context.

It is precisely because of the existence of variable promotion in JavaScript, which leads to a lot of code that is not intuitive, which is also an important design flaw of JavaScript.

Although ECMAScript 6 (hereafter referred to as ES6) has avoided this design flaw by introducing block-level scopes and using the let and const keywords, due to JavaScript’s need to maintain backward compatibility, variable promotion will continue for quite some time. This also makes it more difficult for you to understand the concept, because to understand both the new mechanism and the variable promotion mechanism, the key point is that these two mechanisms run in “one” system at the same time.

Scope

Scope refers to the area where variables are defined in the program, and this location determines the lifecycle of variables. Popularly understood, scope is the accessible scope of variables and functions, that is, scope controls the visibility and lifecycle of variables and functions.

Before ES6, ES had only two scopes: global scope and function scope.

Objects in the global scope can be accessed anywhere in the code, and their lifecycle accompanies the lifecycle of the page.

  • Function scope is the variable or function defined inside the function, and the defined variable or function can only be accessed inside the function. After the execution of the function, the variables defined inside the function will be destroyed.

Before ES6, JavaScript only supported these two scopes. In contrast, other languages generally supported block-level scopes. A block-level scope is a piece of code wrapped in a pair of curly braces. For example, a function, a judgment statement, a loop statement, and even a single {} can be regarded as a block-level scope.

Simply put, if a language supports block-level scope, the variables defined inside its Code Block cannot be accessed outside the Code Block, and after the code execution in the Code Block is completed, the variables defined in the Code Block will be destroyed.

ES6

ES6 introduced the let and const keywords, allowing JavaScript to have block-level scope like other languages.

1
2
3
4
let x = 5
const y = 6
x = 7
Y = 9//error, the variable declared by const cannot be modified

As you can see from this code, the difference between the two is that variables declared with the let keyword can be changed, while variables declared with const cannot have their values changed. But either way, both can generate block-level scopes. For simplicity, in the following code, I use the let keyword uniformly to demonstrate.

So next, let’s analyze how ES6 solves the above problem through block-level scope through a practical example. You can first refer to the following code for variable promotion:

1
2
3
4
5
6
7
8
function varTest() {
var x = 1;
if (true) {
Var x = 2;//same variable!
console.log(x); // 2
}
console.log(x); // 2
}

In this code, there are two places where the variable x is defined, the first place is at the top of the function block, and the second place is inside the if block. Since the scope of var is the entire function, in the compile stage, The following execution context is generated:

https://res.cloudinary.com/dvtfhjxi4/image/upload/v1608036132/origin-of-ray/微信截图_20201215204135_tf6mt8.png

As can be seen from the variable environment of the execution context, ** ultimately generates only one variable x, and all assignments to x in the function body will directly change the x value ** in the variable environment.

So the above code finally outputs 2 through console.log (x), while for the same logical code, the last output value of other languages should be 1, because the declaration inside the if block should not affect the variables outside the block.

Since supporting block-level scope and code execution logic that does not support block-level scope are different, let’s transform the above code to support block-level scope.

1
2
3
4
5
6
7
8
function letTest() {
let x = 1;
if (true) {
Let x = 2;//different variables
console.log(x); // 2
}
console.log(x); // 1
}

Executing this code, the output result is consistent with our expectations. This is because the let keyword supports block-level scope, so in the compile stage, the JavaScript engine does not store the variables declared by let in the if block into the variable environment, which means that the keywords declared by let in the if block will not be promoted to full function visibility. So the value printed inside the if block is 2, and after jumping out of the block, the printed value is 1. This is very consistent with our programming habits: variables declared within the scope block do not affect variables outside the block.

JavaScript

You already know that the JavaScript engine implements function-level scope through the variable environment, so how does ES6 implement support for block-level scope on the basis of function-level scope? You can take a look at the following code first:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

function foo(){
var a = 1
let b = 2
{
let b = 3
var c = 4
let d = 5
console.log(a)
console.log(b)
}
console.log(b)
console.log(c)
console.log(d)
}
foo()

When executing the above code, the JavaScript engine will first compile it and create an execution context, and then execute the code in order. We have analyzed how to create an execution context in the previous article, but now the situation is a bit different. The same, we introduced the let keyword, the let keyword will create a block-level scope, so how does the let keyword affect the execution context?

The first step is to compile and create the execution context. Here is the execution context schematic I drew:

https://res.cloudinary.com/dvtfhjxi4/image/upload/v1608036132/origin-of-ray/微信截图_20201215204144_dzlkwq.png

From the chart above, we can draw the following conclusions:

The variables declared by var inside the function are all stored in the variable environment during the compile stage.

Variables declared through let are stored in the Lexical Environment during the compile phase.

Inside the scope block of function, variables declared through let are not stored in the lexical environment.

Next, the second step continues with the executable code. When executing into the Code Block, the value of a in the variable environment has been set to 1, and the value of b in the lexical environment has been set to 2. At this time, the execution context of the function is As shown in the figure below:

https://res.cloudinary.com/dvtfhjxi4/image/upload/v1608036133/origin-of-ray/微信截图_20201215204151_izno2z.png

As can be seen from the figure, when entering the scope block of function, the variables declared by let in the scope block will be stored in a separate area of the lexical environment. The variables in this area do not affect the scope block. Variables outside, such as variable b declared outside the scope, also declared variable b inside the scope block, exist independently when executed inside the scope.

In fact, inside the Lexical Environment, a small stack structure is maintained. The bottom of the stack is the outermost variable of the function. After entering a scope block, the variables inside the scope block will be pressed to the stack top; when the scope is executed After that, the information of the scope will pop up from the stack top, which is the structure of the Lexical Environment. It should be noted that the variables I am talking about here refer to variables declared through let or const.

Next, when executing the console.log (a) line of code in the scope block, you need to search for the value of the variable a in the lexical environment and the variable environment. The specific search method is: along the stack of the lexical environment. Top down query, if found in a block in the lexical environment, it will be returned directly to the JavaScript engine, if not found, then continue to search in the variable environment.

https://res.cloudinary.com/dvtfhjxi4/image/upload/v1608036133/origin-of-ray/微信截图_20201215204157_g15aka.png

Summary

That is to say, the design of JavaScript at the beginning is only the global scope and function scope, corresponding to the global execution context and function execution context. During the execution of the function, we first look for variables in our own execution context. If we cannot find them, we will Sequentially search along the context in the call stack until the global execution context.

Later, in order to introduce the block-level scope, we created a new stack structure in the context, called the lexical environment. Whenever we encounter a block-level scope, we stack it in the lexical environment, and put the block-level scope. Variables declared using let and const in the scope are put into it, while variables declared by var are still placed in the variable environment, which can not only achieve block-level scope, but also be compatible with var’s variable promotion.