In ECMAScript6, the let and const keywords were added to declare variables. In front-end interviews, we are often asked the difference between let, const and VAR, which involves variable promotion, temporary dead zone, etc. Let’s take a look at variable promotions and temporary dead zones.

1. What variable is promoted?

Let’s take a look at the description of variable promotion in MDN:

Variable promotion (as of 1997) is considered an understanding of how execution contexts (especially the creation and execution phases) work in Javascript. In JavaScript documentation prior to ECMAScript® 2015 Language Specification, the word variable promotion (reactoras) is not found.

In the literal sense of the concept, “variable promotion” means that declarations of variables and functions are moved to the front of the code at the physical level, but this is not accurate. The actual location of variable and function declarations in the code is not moved, but is put into memory at compile time.

Generally speaking, variable promotion refers to the behavior of the JavaScript engine to promote the declaration of variables and functions to the beginning of the code during the execution of JavaScript code. When a variable is promoted, it is set to undefined by default. It is the very nature of JavaScript to promote variables that leads to a lot of non-intuitive code, and this is a design flaw of JavaScript. ECMAScript6 has gotten around this design flaw by introducing block-level scopes and using the let, const keyword, but variable promotion will continue for a long time due to downward compatibility in JavaScript.

Before ECMAScript6, JS engines used the var keyword to declare variables. In the var era, wherever a variable declaration is written, it always ends up at the top of the scope. Declare a num variable in global scope and print it before declaring it:

console.log(num) 
var num = 1
Copy the code

Undefined, because the declaration of the variable has been enhanced, is equivalent to:

var num
console.log(num)
num = 1
Copy the code

As you can see, num is promoted to the top of the global scope as a global variable.

In addition, there is variable promotion in function scope:

function getNum() {
  console.log(num) 
  var num = 1  
}
getNum()
Copy the code

Undefined is also printed here, because the variable declaration inside the function is raised to the top of the function scope. It is equivalent to:

function getNum() {
  var num 
  console.log(num) 
  num = 1  
}
getNum()
Copy the code

In addition to variable promotion, functions are actually promoted. A named function in JavaScript can be declared in one of two ways:

// Function declaration:
function foo () {}
// Declaration of variable form:
var fn = function () {}
Copy the code

When a function is declared in variable form, it is promoted to the top of the scope, and the declaration content is promoted to the top of the scope, as is the case with normal variables. As follows:

fn()
var fn = function () {
	console.log(1)}Uncaught TypeError: fn is not a function

foo()
function foo () {
	console.log(2)}// Output: 2
Copy the code

As you can see, if you declare fn in variable form and execute before it, you will get an error that fn is not a function, because fn is only a variable and has not yet been assigned to a function, so the fn method cannot be executed.

2. Why is there variable promotion?

Variable promotion is closely related to the compilation process of JavaScript: JavaScript, like any other language, goes through compilation and execution phases. During this short compilation phase, the JS engine collects all variable declarations and validates them in advance. The rest of the statements need to wait until the execution phase, when a specific sentence is executed, to take effect. This is the mechanism behind variable promotion.

So why does variable promotion exist in JavaScript?

Start with scope. Scope is the area in the program where variables are defined, and the location determines the lifetime of the variables. In popular understanding, scope is the accessible scope of variables and functions, that is, scope controls the visibility and life cycle of variables and functions.

Prior to ES6, scopes were divided into two types:

  • Objects in global scope can be accessed anywhere in the code, and their life cycle is accompanied by the life cycle of the page.
  • A function scope is a variable or function defined inside a function and can only be accessed inside the function. After the function is executed, the variables defined inside the function are destroyed.

In contrast, other languages generally support block-level scopes. A block-level scope is a piece of code wrapped in braces, such as a function, a judgment statement, a loop statement, or even a single {} can be considered a block-level scope (note that {} in an object declaration is not a block-level scope). To put it simply, if a language supports block-level scope, variables defined inside a code block are not accessible outside the code block, and the variables defined in the code block are destroyed after the code execution in the code block is complete.

ES6 previously did not support block-level scope. Without block-level scope, it is the fastest and easiest design to uniformly promote variables within the scope. However, this directly results in the variables in the function, no matter where they are declared, are extracted into the variable environment of the execution context at compile time. So these variables can be accessed anywhere within the whole function body, which is called variable promotion in JavaScript.

Using variable promotion has two benefits:

(1) Improve performance

Before the JS code is executed, it is syntax-checked and precompiled, and only once. This is done to improve performance. Without this step, the variable (function) must be reparsed every time the code is executed. This is unnecessary because the code does not change.

During parsing, precompiled code is also generated for the function. During precompilation, it counts which variables are declared, which functions are created, and compacts the function code to remove comments, unnecessary white space, and so on. The advantage of this is that you can allocate stack space directly to the function each time it executes (no need to parse again to get what variables are declared and what functions are created), and the code executes faster because of code compression.

(2) Better fault tolerance

Variable promotion can improve JS fault tolerance to some extent, see the following code:

a = 1;
var a;
console.log(a); / / 1
Copy the code

These two lines of code would have reported an error if there had been no variable promotion, but because there was, the code would have executed.

While this can be avoided entirely during development, sometimes the code is so complex that it can be inadvertently used before it is defined, and the code will work just fine because of the variable promotion. Of course, in the development process, it is important to avoid using variables before declaring them.

Conclusion:

  • Improved declarations during parsing and precompilation can improve performance by allowing functions to pre-allocate stack space for variables at execution time;
  • Declaration promotion can also improve the fault tolerance of JS code, so that some non-standard code can be executed normally.

3. Problems caused by variable promotion

Because of variable promotion, using JavaScript to write code with the same logic as other languages can lead to different execution results. There are two main cases.

(1) The variable is overwritten

Take a look at the following code:

var name = "JavaScript"
function showName(){
  console.log(name);
  if(0) {var name = "CSS"
  }
}
showName()
Copy the code

Undefined instead of “JavaScript”. Why?

First, when the showName function call is just executed, the execution context for the showName function is created. After that, the JavaScript engine starts executing the code inside the showName function. The first implementation is:

console.log(name);
Copy the code

The name variable is used to execute this code, and there are two name variables in the code: one in the global execution context with a value of JavaScript; The other is in the execution context of the showName function, since if(0) is never true, the name value is CSS. So which one to use? Variables in the function execution context should be used first. Because during function execution, JavaScript preferentially looks for variables in the current execution context, the current execution context contains the variable name in if(0) whose value is undefined due to variable promotion, so the value of name is undefined.

The output here is not quite the same as that of other languages that support block-level scope, such as C, which outputs global variables, so this can be misleading.

(2) Variables are not destroyed

function foo(){
  for (var i = 0; i < 5; i++) {
  }
  console.log(i); 
}
foo()
Copy the code

When you implement similar code in most other languages, I is destroyed after the for loop ends, but in JavaScript code, the value of I is not destroyed, so 5 is printed. This is also caused by variable promotion. Variable I was promoted during the creation of the execution context, so when the for loop ends, variable I is not destroyed.

4. Disable variable promotion

To address these issues, ES6 introduced the let and const keywords, enabling JavaScript to have block-level scope just like other languages. Let and const do not have variable promotion. Let is used to declare variables:

console.log(num) 
let num = 1

Uncaught ReferenceError: num is not defined
Copy the code

If we change to const declarations, we get the same result — variables declared with lets and const are declared at the same time that the specific code is executed.

The variable promotion mechanism can lead to a lot of misoperations: variables that are forgotten to be declared are not obvious at development time, but are hidden in the code as undefined. In order to reduce runtime errors and prevent undefined from causing unpredictable problems, ES6 purposely makes pre-declaration unavailability a strong constraint. However, there is a difference between let and const. A variable declared with the let keyword can be changed. A variable declared with const cannot be changed.

Let’s look at how ES6 solves this problem with block-level scopes:

function fn() {
  var num = 1;
  if (true) {
    var num = 2;  
    console.log(num);  / / 2
  }
  console.log(num);  / / 2
}
fn()
Copy the code

In this code, there are two places where the variable num is defined, the top of the function block, and the inside of the if block. Since the scope of var is the whole function, the following execution context is generated at compile time:

As can be seen from the variable environment of the execution context, only one variable num is generated in the end. All assignments to num in the function body directly change the value of num in the variable environment. So the code above ends up printing 2. For code with the same logic, the last output in other languages should be 1, because declarations inside if should not affect variables outside the block.

Var keyword = let keyword;

function fn() {
  let num = 1;
  if (true) {
    let num = 2;  
    console.log(num);  / / 2
  }
  console.log(num);  / / 1
}
fn()
Copy the code

Execute this code, and the output is as expected. This is because the let keyword supports block-level scoping, so the JavaScript engine does not store the variables declared by the LET in the if into the variable environment at compile time, which means that the keywords declared by the let in the if are not promoted to full function visibility. So the printed value inside if is 2, and after the block, the printed value is 1. This conforms to our convention: variables declared inside the action block do not affect variables outside the block.

5. How does JS support block-level scope

So, how does ES6 support variable promotion and block-level scope? Let’s look at why from an execution context perspective.

JavaScript engines implement functional scope through variable environments, so how does ES6 implement block-level scope support on top of functional scope? Take a look at the following code:

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

When the code executes, the JavaScript engine compiles it and creates the execution context, and then executes the code sequentially. The let keyword creates block-level scopes, so how does the let keyword affect the execution context?

(1) Create execution context

The execution context created looks like this:

It can be seen from the above figure that:

  • Variables declared by var are stored in the variable environment at compile time.
  • Variables declared by let are stored in the lexical environment at compile time.
  • Inside the function scope, variables declared by let are not stored in the lexical environment.

(2) Execute the 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, and the execution context of the function is shown below:As you can see, when entering the function block, the scope of the scope block variables through the let statement, will be stored in a separate lexical environment area, the area of the variable does not affect the outside of the scope of variables, such as in the scope statement face the variable b, within the scope of also declare the variable b, when performing the into the scope, They all exist independently.

In fact, inside the lexical environment, a 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 pushed to the top of the stack. When the scope execution is complete, the information for that scope is popped from the top of the stack, which is the structure of the lexical environment. Variables are declared by let or const.

Next, when executing into the scope blockconsole.log(a)“, you need to find the value of variable A in the lexical environment and the variable environment. The search method: search down the stack of lexical environment, if found in a block in the lexical environment, directly back to the JavaScript engine, if not found, then continue to look in the variable environment. This completes the variable lookup:When the scoped block finishes execution, its internally defined variables are popped from the top of the lexical environment’s stack. The final execution context looks like this:Block-level scope is implemented through the stack structure of the lexical environment, while variable promotion is implemented through the variable environment. By combining the two, the JavaScript engine supports both variable promotion and block-level scope.

6. Temporary dead zones

Finally, the concept of temporary dead zones:

var name = 'JavaScript';
{
	name = 'CSS';
	let name;
}

// Uncaught ReferenceError: Cannot access 'name' before initialization
Copy the code

ES6 states that if a block contains let and const, the variables declared by the block for those keywords form a closed scope from the start. If you try to use such variables before declaration, you get an error. The area where the error occurs is the temporary dead zone. The area above line 4 of the code above is the temporary dead zone.

If we want to successfully reference the global name variable, we need to remove the let declaration:

var name = 'JavaScript';
{
	name = 'CSS';
}
Copy the code

The program will then run normally. In fact, this does not mean that the engine does not know that the name variable exists; on the contrary, it does, and it clearly knows that name is declared in the current block with a let. That’s why it puts a temporary dead zone on this variable. Once you remove the let keyword, it doesn’t work either.

This is the essence of a temporary dead zone: when a program’s control flow is instantiated in a new scope, variables declared as lets or const in that scope are created in the scope first, but are not yet lexical bound, so they cannot be accessed, and will throw an error if they are accessed. Therefore, the time between the execution process entering the scope to create a variable and the variable being accessible is called a temporary dead zone.

Typeof operators, which were 100% safe before the let and const keywords, can now cause temporary dead zones, as can the way the import keyword introduces public modules and classes are created using a new class, due to variable declaration and use.

typeof a    // Uncaught ReferenceError: a is not defined
let a = 1
Copy the code

As you can see, the typeof keyword was used before the declaration of a, which is caused by a temporary dead zone.