preface

I’m clickbait 😥. But still hope everybody big guy can help see this article still have what did not say or have wrong place, welcome to point out!

If you want to learn JS in depth, scope is a knowledge point that must be understood. I won’t go into too much detail here about what scope is. According to MDN’s simple definition: the current execution context (if you don’t know what an execution context is, check out another article I wrote — From Execution Context to variable promotion). The context in which values and expressions are “visible” or accessible. In layman’s terms, we can think of a scope as a set of rules that govern whether a value or variable is visible or accessible in the current context. Today let’s take a look at what block-level scope is.

Analyze the block-level scope

Why block-level scope

Before let, JS had only global scope and function scope. These are the only scopes that can cause problems in our code writing in some cases. Here’s a typical example

var arr = []
for (var i = 0; i < 10; i++){
    arr[i] = function(){
        console.log(i);
    }
}
arr[2] ()Copy the code

Here, we want each array to hold a function, and each function to output a value corresponding to the subscript, such as 2. But it didn’t turn out that way. We get the following results:

PS D:\Code\LESSON_SS\js> node .\test.js
10
Copy the code

This is because var declares a global variable, and due to the operation mechanism of JS, it cannot find the variable I in the function, so it will look up the higher scope. And when the cycle ends. I becomes 10. Whenever any function in the array of functions prints I, it prints 10.

Therefore, we need a block-level scope that allows us to isolate each loop so that it can output as we wish.

How do I create block-level scopes

In ES6, the let and const directives were added to create a block-level scope. Below, we create a block-level scope

{
    let i;
    i = 10;
}
console.log(i)   / / an error! ReferenceError: I is not defined because I in the block-level scope cannot be found in the global scope
Copy the code

To get a more intuitive view of our block-level scope, we welcome the best tool for programmers – the Debug tool. Use the debugger to set a breakpoint at the very front of the code, and then start the code debugging tool (I’m using vscode+nodejs, but you can also debug using a browser). As follows:

debugger
{
    let i 
    i = 10
}
console.log(i);
Copy the code

After starting debugging, we get the following results

Here we can see that the current Local scope does not have the I variable declared, then we proceed to click next:

Here, we can clearly see that the code goes inside curly braces, the new scope Block appears, and we can also see that the variable I is declared in the new scope and set to undefined initially. As we move down, we see that the Block scope disappears after the I = 10 step.

This is because I is completing the assignment operation, and the block-level scope directly ends because there is no other code to execute later, and the code correspondingly jumps out of the block-level scope, so we cannot observe the variables in the block-level scope. If we add another operation after I =10, we can see the change in I. Here we change the code to the following

{
    let i 
    i = 10
    i = 0
}
console.log(i);
Copy the code

The result is as follows:

We can see from the debugging tools that let does create a block-level scope in the global scope. At the same time, the global scope does not have access to the contents of the block-level scope as a result of the run.

Characteristics of block-level scopes

Variable free lifting

The block-level scope is proposed not only to remedy the problems mentioned above, but also to solve the problem of variable promotion. Due to the operation mechanism of JS, JS will produce a behavior called variable promotion in the execution process, that is, all variable declarations and function declarations will be promoted to the front of the current scope. This allows us to use variables before declaring them when we’re writing code.

However, variable promotion is not logical in theory, and can be considered as a bug of JS operation mechanism. Let and const solve this problem nicely.

Next, let’s debug to see how variable promotion occurs and how let and const fix the variable promotion bug. We write the following code:

debugger
j = 10;
console.log(j);
var j;
{
    i = 10;
    console.log(i);
    let i;
}
Copy the code

Using the compiler debugging, we get the following results

Var j = undefined; var j = undefined; var j = undefined; Also check out my previous article — from execution Context to variable promotion). Then we proceed down:

When console.log(j) is executed, the assignment of j is complete and the value of j changes to 10. Such code does not generate errors due to variable promotion. Next, let’s enter the block-level scope:

As you can see, there is no declaration that generates the block-level scoped variable Ilet iIt doesn’t look likevar iWe can observe that the execution went wrong:

ReferenceError: i is not defined
    at Object.<anonymous> (D:\Code\LESSON_SS\js\test.js:6:13)
    at Module._compile (internal/modules/cjs/loader.js:1063:30) at Object.Module._extensions.. js (internal/modules/cjs/loader.js:1092:10)
    at Module.load (internal/modules/cjs/loader.js:928:32)
    at Function.Module._load (internal/modules/cjs/loader.js:769:14)
    at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:72:12)
    at internal/main/run_main_module.js:17:47
Copy the code

This also shows that variables declared by let do not have variable promotion.

According to Ruan Yifeng’s explanation in ES6, the variable is not available before the let command is used to declare it in the code block. This is grammatically known as a “temporal dead zone” (TDZ). That is, until I uses the let declaration, I is in its temporary dead zone, and we have no access control over changes, which makes up for the problem of variable escalation.

Duplicate declarations are not allowed

Variables declared by let are not allowed to be declared twice in the same scope. The following code

// declared: SyntaxError: Identifier 'a' has already been declared
{
    let a = 1;
    var a = 10;
}
// Also generates an error
{
    let b = 1;
    let b = 2;
}
Copy the code

Block-level scope in a loop

Classic test cases

Use global scope

Let’s go back to the code at the beginning of this article:

var arr = []
for (var i = 0; i < 10; i++){
    arr[i] = function(){
        console.log(i);
    }
}
arr[2] ()Copy the code

Again, using the debug method, let’s see what happens (don’t forget to set a breakpoint).

At the beginning, we can see that the array arr and variable I have been raised due to the promotion. Both variables are visible in the current scope before the code is executed, and the initial value is undefined. Further down, we come to the loop:

As you can see, when both ARR and I are initialized, we are always working in a scope. Here we determine that I <10(because I is 0) and we start the loop:

So here we can see that in the loop, we’re saving the function in an array, and in the saved function scope chain, we can see that there’s a scope for the closure that holds the value of I. That might seem a little bit normal, because I is 0. And then we go to the next loop, i++. We can see the following results:

After I ++, we can see the trick. The I in arr[0] is supposed to print 0, but after I ++, the I inside the function also becomes 1. Then we go ahead and do it (I’ll do it a few more times to get a better effect).

I’ve done it five times, I’s four, I’ve got five functions in my array, and now let’s look at what’s in those five functions.

It can be found that no matter which function is in the scope chain, there is a closure’s scope. In the scope, there is an I, whose value changes with the I in the outer loop. That’s why the final output is always 10.

Next, we continue executing until the loop completes and we start calling functions in the array:

We can clearly see that when the function is executed, the function in the array is pushed onto the call stack and the function is executed. Here we can see that within the scope, there is a closure scope, and that scope holds I. In addition, the value of I is 10 because of cyclic accumulation, so when the function executes, it does not find variable I in the current scope (here is Local:arr.

in the figure), so it looks up (the scope display here is displayed according to the stack; if the current scope cannot be queried, it looks down). It finds the Closure scope, finds I in that scope, and prints. The result is that every function that prints I is the output I stored in the global scope and in the closure generated by the function.

Use block-level scopes

Here we simply change the var declaration to let declaration in the code:

debugger
var arr = []
for (let i = 0; i < 10; i++){
    arr[i] = function(){
        console.log(i);
    }
}
arr[2] ()Copy the code

As soon as the code executes, we see something like this:

Here we see a clear difference from var declaring I. I did not find the declaration in the current scope. Let’s move on to the next step:

As you can see here, when the loop statement is executed, a block-level scope is created because of the let declaration. The block-level scope holds the value of I. Next, we loop several times to see how the array function changes

You can see that there is a block-level scope for each execution, and you can see that, unlike var declarations, there is no longer a closure scope in the scope chain for each function, but a block-level scope. And the value of I is different in each block-level scope. As follows:

Finally, let’s call the execute array function and see what happens?

Similar in form to the previous code, but different in content, no longer the closure scope, but the block level scope, the value of I is not affected by the global variable I. The code does what it wants and prints 2.

An interesting little piece of code

Finally, let’s take a look at an interesting little piece of code to take a closer look at the block-level scope in the body of the loop

debugger
for(let i = 0; i<5; i++){let i;
    i = 1;
    console.log(i);
}
Copy the code

So let’s see, what happens to this code? Infinite loop? An error?

The correct result is as follows:

PS D:\Code\LESSON_SS> node "d:\Code\LESSON_SS\js\test.js"
1
1
1
1
1
Copy the code

JS has sent us five ones. Why is that? Next, let’s go ahead and use the debug tool to see what happens to it.

When the code executes into the parenthesis statement in the loop. We saw the generation of a block-level scope. The results are as follows:

Then run down:

We can see that when running into the loop body, and created a new class scope, it also explains why won’t the cause of the error, the execution result because parentheses and curly brackets belong to two different scope, though they have the same variable names, but is not the same scope, so repeat the statement did not happen.

We continue to execute:

I is assigned the desired value of 1, and the console outputs, and we go to the next loop:

We find that before the variable I is declared, I already exists and is assigned an initial value of 1. Is this 1 given by the I in parentheses or passed by the body of the last loop execution? So, proceed down:

At this point, the block-level scope in parentheses has I 2, but the initial value of I in curly braces is still 1. At the same time we noticed a strange phenomenon when the code continued to execute, encounteredlet iDeclaration, I becomes undefined again. As follows:

It is true that we have been confused for a long time, but after looking up information and carefully looking at Teacher Ruan Yifeng’s ES6 introduction, teacher Ruan Yifeng mentioned:

The JavaScript engine internally remembers the value of the previous cycle, and when it initializes the variable I of this cycle, it evaluates on the basis of the previous cycle.

I’ve seen it on the Internet, that in each cycle, the value of the cycle is passed to the next cycle, and that’s why in each cycle, the value of I can be found in scope with an initial value.

At the same time, by studying Ruan Yifeng’s introduction to ES6, we know that there is a block-level scope inside parentheses, and there is also a block-level scope inside curly braces. The two scopes have a parent-child relationship, and when some variables in the child scope (the parenthes-wrapped scope) cannot be found, the parent scope is searched (the parenthes-wrapped part).

This is why the code ends up printing five ones instead of an infinite loop and not an error, because (let I = 0; i < 5; I++) is a scope, while {code… } is another scope. It does not matter if two scopes declare the same variable name.

summary

When learning block-level scope, especially the block-level scope in the loop, I feel that my understanding of block-level scope is not very clear. Through the debug tool and learning teacher Ruan Yifeng’s document, the block-level scope is more clear. However, the above content is my own personal opinion in the process of learning, through information query, ask the teacher, it is difficult to ensure that the content of the article is 100% correct, so I hope you find mistakes in the article after pointing out in the comment section, thank you!