Let ES6 in introducing a new variable declarations keyword | const, in order to solve some var period historical burden. But no matter in the past or now, as well as in the future, the shadow of VAR will continue to exist in many JS programs cannot be stripped. So let | const is a kind of compatibility of compromise, and this kind of “compromise” will continue to in the process of evolution of ES perfect continues. Let can now be seen as a new age alternative to VAR.

A = window.a = undefined. A = window.a = undefined.

Take a look at let’s past life.

1. Claim promotion (as of 1997)

Let’s start with the following code:

fn();
console.log(variable); // undefined
var variable = 1;
function fn(){
  console.log(variable); // undefined
}
console.log(variable); / / 1

Copy the code

The output of the executed code is: undefined undefined 1. According to the premise that JS statements are executed in sequence, the first two lines of code are executed before fn function and variable variable are declared, so errors should be thrown. The results obtained are not intuitive. This phenomenon is called function | variable declarations. At the same time, this phenomenon is a reminder that something happened at some point before the JS statement was executed, namely that variable declarations and function declarations were processed ahead of time, known as promotion.

We simply divide JS statements into declaration statements and other statements. Before ES6, declarative statements actually only var and function, but in the later, this team has made great extend, like let | const | class and asynchronous and iterator function such as this, aside.

A section of JS code from the beginning to the end of the life cycle will first go through a “pre-parsing” process. Take the above code as an example. Assuming the code is in the global scope, the JS engine will find all the declarations in the current scope and allocate memory space for the variables in the following assignment process. If they are basic type values (variable), they will be stored in the stack memory; if they are reference type values (FN), they will be stored in the heap memory.

Var variable = 1; For example, the engine first creates variable and initializes its value to undefined. Finally, the expression variable = 1 assigns its value to the number 1. Fn is declared and initialized to the function itself. In the pre-parsing phase, the value of variable in memory is undefined, and all var declarations will have the same step. The value of fn in memory at this stage is the declared function itself. So you can roughly modify the above example as follows:

var variable = undefined;
function fn(){
  console.log(variable); // undefined
}

fn();
console.log(variable); // undefined
variable = 1;
console.log(variable); / / 1

Copy the code

However, “lifting” does not change where the code is actually written, it just allows executable statements to reach variables in scope. This is exactly how the current execution context is created.

With that in mind, let’s look at the following code:

var a = 0;
function fn2() {
  console.log(a); // undefined
  // ...
  if (false) {
    var a = 2;
  }
  // more code
  console.log(a) // undefined
}

fn2();

Copy the code

When the function fn2 is executed, it also undergoes a precompilation process, and the declaration of variable A in the function body also undergoes “promotion”, so when the executable statement of the function body is executed, variable A can already be found in the scope of fn2, and its initial value is undefined. It no longer looks up the scope chain to its upper scope. Because of the if false condition, the assignment statement a = 2 in the if branch will not be executed, resulting in the printed value of the second variable a being undefined. This is the opposite of what we expected — a print value of 2.

Thus, if we have a very complex piece of code, because promotion “gets in the way”, we may inadvertently overwrite the a value that would actually be useful, resulting in an unexpected bug. This is one of the reasons for advocating var declarations as close as possible, and it is one of the meanings of semantically compliant naming conventions.

The reason is also due to the problem of scope. In earlier JS languages there was no concept of block-level scope, only a simple division of scope inside and outside a function, so the second declaration of variable A in Fn2 was promoted outside the if branch statement block, causing some obscure bugs. Because essentially, this declaration of a is in the body of the fn2 function, the scope of the function.

This is where the classic, counterintuitive for loop problem comes in. Look at the following code:

var msgLs = ['hello'.'for'.'loop'];

for(var i = 0, len = msgLs.length; i < len; i ++) {
  console.log(msgLs[i], 'follow loop');
  setTimeout(function() {
    console.log(msgLs[i], 'follow timer'); // undefined x 3
  }, i * 500);
}

Copy the code

The expected result is that each console prints hello for loop in sequence, but this is not the case.

Given that this code is executed at the top level, it is obviously scoped out of the function, so the variables msgLs and I and len go through the promotion process, and then I is reassigned four times during the execution of the body of the for loop, ending at 3. There is only one I variable in global scope at all times, so it is easy to understand why three undefined variables are printed.

To solve this problem, IIFE: Immediately Invoked Function Expression was proposed:

var msgLs = ['hello'.'for'.'loop'];

for(var i = 0, len = msgLs.length; i < len; i ++) {
  console.log(msgLs[i], 'follow loop');
  (
    function iife(i) {
      setTimeout(function() {
        console.log(msgLs[i], 'follow timer');
      }, i * 500);
    }
  )(i)
}

Copy the code

To achieve the desired output. This brings up another honey concept closure, which I’ll talk about next time.

Commonly written as two consecutive parentheses ()(), IIFE was used in the early years as a means of modularizing code functions to expose limited variables (namespaces) and prevent possible global variable contamination.

Finally, the implementation of block-level scope in ES6 and the arrival of many more new features should take the hassle out of it.

Block-level scope

We modify the above code using let as follows:

let a = 0;
function fn2() {
  console.log(a); / / 0
  // ...
  if (false) {
    let a = 2;
  }
  // more code
  console.log(a); / / 0
}

fn2();

Copy the code

It can be seen that the repeated declaration of variable A in the conditional branch in fn2 does not affect the result printed twice by the function body. Let (& const) and a block form a new block-level scope that makes the declaration in the conditional branch free of the negative effects of “promotion”, as we expect.

That is to say, usually a pair of curly braces {} and contains a group by the let | const identifier for statement constitutes the block-level scope. Common statements are as follows:

if {} else {}

try {} catch() {}finally{}

with() {}

while() {}

do {} while()

{}

/ / and

for(let x){}

// ...

Copy the code

Block-level scope of form usually has nothing to do with the statement itself, the focus is on the braces, and there were the let | const. Even if it’s something as simple as this:

let a = 0;
{
  let a = 1;
  console.log(a); / / 1
}
console.log(a); / / 0

Copy the code

Block-level scopes are also formed. There is a lexical mechanism behind the question of whether braces {} express the semantics of a block or an object literal.

But there is a special point here, namely, for… let… Structure for a loop statement.

const arr = [];
for(let i = 0; i < 10; i++) {
  arr.push(i);
}
console.log(arr);

Copy the code

According to the above understanding, the for loop should also have only one block level scope wrapped by the following block, but ultimately the value of the variable I is exactly preserved in each iteration of the loop. This seems to be another unexpected behavior, so there should be other mechanisms at work. This is another topic, aside:) here, let’s look at the let | const what are the new features.

Let /const declaration

Take let as an example to summarize briefly. Const has much the same characteristic, except that the const keyword declares a constant and must be given a value that cannot be changed later.

Block-level scope

The first point, which was answered in the “block-level scope” section above, is that, in contrast to var, using let declarations in statement blocks creates block-level scopes, and the parent scope is not accessible to identifiers declared in the current scope.

Temporary dead zone

console.log(a); // Uncaught ReferenceError: Cannot access 'a' before initialization
let a;

Copy the code

The above code fails to access a variable that has not been initialized. This phenomenon is known as a temporary Dead Zone. Thus, the language requirement that we declare variables before we use them would presumably avoid a lot of unnecessary bugs (and it does).

But does this mean that there is no promotion for variables declared using let? It really doesn’t seem to exist, but on second thought, if there is no improvement, what is the TDZ error? So just as with var declarations, something must have happened during the JS engine’s pre-processing of code, leading to TDZ.

As we learned in the claim promotion (colliding) section above, the process of promotion is actually the process of creating the executable code execution context, as well as the process of forming the current scope. Meanwhile, as the error reminds us: “A” has not been initialized. Instead of a not defined yet.

let a = 'this is an outer a';
{
  console.log(a); // Uncaught ReferenceError: Cannot access 'a' before initialization
  let a = 'this is an inner a';
}

Copy the code

The above code can also support one or two, if the previous deduction is not true, then the print of variable A should continue along the scope chain to search for the value of its parent scope, and print this is an outer a, but also gives a reference type error.

So you can assume that the variable declared by the let is not unpromoted; it is created, just uninitialized.

let a = 'this is an outer a';
{
  // a: TDZ start
  console.log(a); // Uncaught ReferenceError: Cannot access 'a' before initialization
  let a; // a: TDZ end
}

Copy the code

You cannot duplicate declarations in the same scope

let a = 'first declare';
let a = 'seconed declare';
// Uncaught SyntaxError: Identifier 'a' has already been declared

Copy the code

Repeating declarations of variables using let gets a syntax error: identifier A is already declared. This also helps us avoid bugs in variable declarations that we might inadvertently cause.

Enables the for loop to retain the current variable value for each iteration

Use the same code example from the first section:

const msgLs = ['hello'.'for'.'loop'];

for(let i = 0, len = msgLs.length; i < len; i ++) {
  console.log(msgLs[i], 'follow loop');
  setTimeout(function() {
    console.log(msgLs[i], 'follow timer');
  }, i * 500);
}

Copy the code

Unlike var, the addition of let makes the printed values in the timer also conform to our expectations. Therefore, it seems that there are other scopes outside the scope of the loop statement that hold different variable values generated each time through the loop and persist in memory. This explains why the value is still accessible after the loop ends.

Does it feel like there’s a connection to “closures” as well? But that’s just a guess, and I’ll leave it to the closure summary to learn more about TA ~

Not accessible in the global object

Ok, finally to the point. The variable declared by the let at the top level is not bound to the Global Object, so where does it go?

Some concepts about the JS lexical environment

JavaScript’s Lexical Environment is associated with an Environment Records, which Records the various identifiers declared by the code in the Lexical Environment associated with the current scope. This Environment record can be roughly divided into Declarative Environment Records and Object Environment Records. The former records the identifiers declared in the current scope, such as variable, constant, let, class, module, import, function, etc. The latter is associated with a binding object and holds a string identifier name corresponding to the property name of the binding object, whose values can be appended and changed dynamically.

Object environment records are rare. An example of this is the with statement, which generates an object environment record for an object passed in, so within the with block, you can use the property name of that object to get the corresponding value.

const obj = { a: 1 };
with(obj){
  console.log(a); / / 1
}

Copy the code

Typically, there are Global Environment Records in the Global Environment, associated with a Global Object that binds predefined properties and methods, It also allows users to add properties and methods to it at a later time, and even change the default values. Such as the var declaration and the funciton function declaration, whose identifiers are bound to the global object as properties. In the browser environment, it is called Window or globalThis.

The implementation of the global environment record is an encapsulation of both the declared environment record and the object environment record, where the “default properties and methods” belong to the object environment record and the subsequent attributes and methods added by the user belong to the declared environment record. But in the process of the user to add attributes, there will be a judgment whether the lexical statement (let | const | class, etc.), if is lexical statement, is not bound to the global object. This explains why variables declared globally with the let keyword are not accessible through window.a.

But we still have the second part of the question: where? Since there is a judgment operation, in the case of true judgment, there must be another destination.

Starting with ES6, there is a separate declarative Environment record, distinct from the global Environment record, which is associated with a Lexical Environment Object to store identifier declarations that are not made by the var function keyword and are not visible.


The original article poke me ~

END