Scope, execution context, the scope chain

The scope is the visible area of ​​a function or variable. In layman's terms, functions or variables cannot be accessed if they are not in this area.

JavaScript is different from most other high-level languages. For example, the Java language has a block-level scope, which is determined by the position of a curly brace {...}, but before ES6, Javascript is not like this. It uses functions. Domain and global scope, until the emergence of ES6, there is a block-level scope. Many front-end engineers have written code for 3 years, but in fact, they may not be able to clearly explain what scope is and what is scope chain, especially the concept of execution context is not clear. If you are not clear about these three basic concepts, you will lose the slightest bit and fallacy in using JavaScript to do the project later. The more I do, the more confused I am. I often stay at only writing, or I have accumulated some experience after debugging many times, but I can’t understand the reasons by analogy, and I can’t respond quickly to a change.

1.1 Function scope The area (ellipsis...) wrapped with code similar to function(){...} in the form of a function is the function scope. The concept corresponding to function scope is global scope, that is, variables or functions defined in the outermost layer can be accessed anywhere. Front-end engineers should have already learned and familiarized with this concept in the introductory stage, so we will not repeat them with examples. We will focus on function scope later.

var a = "coffe"; //In the global scope
function func(){
var b="coffe"; //In the function scope
console.log(a);
}
console.log(a); //>> coffe
console.log(b); //>> Uncaught ReferenceError: b is not defined
func(); //>> coffe

As above, a is defined in the global scope and can be seen anywhere, so a can be accessed in the function, and b is defined in the function, the visible area is the function code block, and the following print command console.log(b) is in the function Execution outside of cannot access b in the function func, so it outputs Uncaught ReferenceError: b is not defined. Any code snippet is wrapped with a function on the outside as if a protective cover is added, which can hide the internal variables and functions, and the internal content cannot be accessed from the outside. The above example shows one way: you can use functions to hide something. This method is very useful in daily development!

// Global scope

function func(){  //Scope A
var a = "coffe";
function func1(){  //Scope B. Define a function to hide the content that you don’t want to make public
var a = "1989";  //The a here covers the value of a in the outer layer
var b = "b";
//There can be many other things to hide: variables or functions
//...
//...
console.log(a);
}
console.log(a); //>> coffe
console.log(b); //>> Uncaught ReferenceError: b is not defined
func1(); //>> 1989
}

The above example illustrates a nested function, which is equivalent to the scope A of the outer function func, and the scope B of the function func1 is embedded. When the print command console.log(a) in func1 accesses the variable a the JS engine will first lookup the variable a from the scope A closest to itself, and will not continue to search if it finds it, and go to the upper scope if it cannot find it (this In the example, the upper scope is the global scope) continue to search. In this example, a has been found and the value is "coffe", so coffe is printed out. By analogy, the execution of func1() will execute the console.log(a) inside the func1 function, and then look for a in scope B, and there is a declaration and assignment statement of a in scope B. var a = "1989", So the value of a first found is 1989, and if found, it will not continue to search, and finally func1() outputs 1989 instead of coffe. But every time you have to define a "function name with no duplicate name" and place it in the upper-level scope, it seems a bit of a waste of memory space, and it is a headache to think of a name that is not repeated. So it is best to wrap it in the form of an anonymous function and execute it immediately, that is, IIFE. The following example:

//Global scope
function func(){  //Scope A
var a = "coffe";
(function(){  //Scope B. A function in the form of IIFE, which hides the content that you do not want to disclose
var a = "1989";
var b = "b";
//There can be many other things to hide: variables or functions
//...
//...
console.log(a);
})();  //>> 1989
console.log(a); //>> coffe
console.log(b); //>> Uncaught ReferenceError: b is not defined
}

As above, use an IIFE plus anonymous function to hide the variable b, so that the outside of the function cannot access it, but the inside of the function can access it. This book recommends that you try to wrap the code fragments to be debugged with anonymous functions at all times, and then execute them immediately in the form of IIFE, and this convention will be followed later in this book.

ES6 brings block-level scope ES6 stipulates that variables and functions declared with the let keyword inside a certain curly brace pair {} have block-level scope. These variables and functions can only be used by the inner statement of the curly brace pair {}, and cannot be used outside. access. When you write the code, the block-level scope of variables and functions has been determined. Block-level scope and function scope can also be collectively referred to as local scope. ES6 introduced block-level scope, which explicitly allows functions to be declared in block-level scope. ES6 stipulates that in the block-level scope, the function declaration statement behaves like let and cannot be referenced outside the block-level scope. However, such processing rules will obviously have a great impact on the old code. For the sake of backward compatibility, functions declared in the block-level scope can still be referenced outside the scope. If you need the function to work only in the block-level scope, you should use the let keyword to write a function expression instead of a function declaration statement. To prove the statement, let's look at a piece of code.

{
function func(){//function declaration
return 1;
}
}
console.log(func());//>> 1

In the above code, the function func is clearly declared inside the curly braces. According to the original ES6 specification, the outside should be inaccessible, but in fact it can. It proves that the JS engine has made workarounds when implementing the ES6 specification for backward compatibility. deal with. Let's look at a piece of code again.

{
var func = function (){//Function expression without let keyword
return 1;
}
}
console.log(func());//>> 1

In the above code, the function func is clearly declared inside the curly braces. According to the original ES6 specification, the outside should be inaccessible, but in fact it can. It proves that the JS engine has made workarounds when implementing the ES6 specification for backward compatibility. deal with. Let's look at a piece of code again.

{
var func = function (){//Function expression without let keyword
return 1;
}
}
console.log(func());//>> 1

The above code has the same effect as the previous code.

{
  let func = function (){
    return 1;
  }
}
console.log(func());//>> func is not defined

The above code proves that the function declared by the let keyword inside the curly braces {} is really in the block-level scope.

Why introduce block-level scope? With global scope and function scope, and var is already very useful, why introduce block-level scope and the keyword let? First of all, it is true that before ES6, the combination of function scope and var is also very useful, but after all, it is not as concise as the block-level scope of {} combined with let! Secondly, the variable declared by var has a side effect: the declaration is made in advance.

(function() {
console.log(a); //>> undefined
console.log(b); //>> ReferenceError
var a = "coffe"; //declare in advance
let b = "1989"; //Variables declared by the let keyword have no advance feature
})();

In the above code, var a = "coffe" contains two operations, one is the declaration of variable a (that is, var a), and the other is an assignment (that is, a = "coffe"). Declaring in advance means that the variable declared with the var keyword can actually be regarded as being declared at the top of the function body, so console.log(a) outputs undefined, which means that the variable has been declared (but not yet assigned). Declaring this feature in advance makes many programmers easily confused. It stands to reason that variables (or functions) can only be read (searched) after they are declared, but var has made this common-sense almost weird, and the appearance of let makes this weird return to common sense. Again, because var declared variables are polluted.

(function() {
for (var i = 0; i <100; i++) {
//...Many lines of code
}
function func() {
//...Many lines of code
}
//...Many lines of code
console.log(i); //>> 100
})();

The i in the loop is useless after the loop is completed, but it is not recycled, but the “garbage” variable that has always existed, polluting the current environment. Using let to declare variables, such garbage variables will be collected soon afterward.

(function() {
for (let i = 0; i <100; i++) {
//...Many lines of code
}
function func() {
//...Many lines of code
}
//...Many lines of code
console.log(i); //>> ReferenceError
})();

In summary, you should use let and avoid var as much as possible, except of course, you want to define a global variable.

2.1 Execution Context definition The execution context is the environment in which the current JavaScript code is parsed and executed, also called the execution environment. It is an abstract concept, which means that we can understand it in our minds, so that we can really master JavaScript in the follow-up, instead of looking for its specific implementation. Any code running in JavaScript runs in the execution context. During the creation phase of the execution context, the variable object (Variable Object, detailed later in this article), scope chain, and this point will be determined respectively.

Types of There are three types of execution contexts:

Global execution context: This is the default and most basic execution context. Code that is not in any function is in the global execution context. It does two things: 1. Create a global object, which is the window object in the browser; 2. Point this pointer to this global object. Only one global execution context can exist in a program. Function execution context: Every time a function is called, a new execution context is created for the function. Each function has its own execution context, but it will only be created when the function is called. Any number of function execution contexts can exist in a program. Whenever a new execution context is created, it will execute a series of steps in a specific order. The specific process will be discussed later in this article.

Eval execution context: The code running in the eval function has also gained its own execution context. The eval function is no longer recommended after ES6, so this book will not discuss eval in-depth for practical interviews.

The life cycle of the execution context The life cycle of the execution context includes three stages: creation stage → execution stage → recycling stage. This article focuses on the creation stage.

a. Creation phase When a function is called, but before any internal code is executed, it will do the following three things: Create a variable object: first initialize the parameter arguments of the function, promote the function declaration and variable declaration (the variable declaration depends on the var keyword in advance). Create a scope chain: In the creation phase of the execution context, the scope chain is created after the variable object. The scope chain itself contains variable objects. The scope chain is used to resolve variables. When asked to parse a variable, JavaScript always starts from the innermost level of code nesting. If the innermost level does not find the variable, it will jump to the parent scope of the previous level to search until it finds the variable. Determine what this points to.

b. Execution phase After the creation is complete, the code will start to execute. At this stage, variable assignments, function references, and other code execution will be completed.

c. Recycling phase After the function is called, the function is popped from the stack, and the corresponding execution context is also popped from the stack, waiting for the garbage collector to reclaim the execution context.

var a = "coffe";  // 1. Enter the global execution context
function out() {
var b = "19";
function inner() {
var c = "89";
console.log(a+b+c);
}
inner(); // 3. Enter the execution context of the inner function
}
out(); // 2. Enter the execution context of the out function

When the code starts to execute, it will first generate a global execution context. When the function is called, the function execution context will be generated. After the function call is completed, its execution context and the data in it will be destroyed, return to the global execution environment, and the web page will be closed. The global execution environment will also be destroyed. In fact, this is a process of stacking and popping. The global context is always at the bottom of the stack, and the current function execution context is at the top of the stack. The execution of the above code will go through the following process:

When the code starts to execute, a global execution context is created, and the global execution context is pushed onto the stack. After the global execution context is pushed onto the stack, the code in it starts to execute, performs assignments, function calls, and other operations. When the execution reaches out(), the activation function out creates its own execution context, and the out function execution context is pushed onto the stack.

After the execution context of the out function is pushed onto the stack, the code in it starts to execute, performs assignments, function calls, and other operations. When inner() is executed, the inner function is activated to create its own execution context, and the inner function execution context is pushed onto the stack. After the inner function context is put on the stack, the code in it starts to execute and performs operations such as assignment, function call, and printing. Since there is no need to generate other execution contexts, after all the code is executed, the inner function context is popped out of the stack.

The inner function execution context pops out of the stack, and returns to the out function execution context environment, and then executes the remaining code in the out function. Since there is no need to generate other execution contexts later, after all the code is executed, the out function execution context is out of the stack .

After the execution context of the out, function is popped from the stack, it returns to the global execution context until the browser window is closed and the global execution context is popped from the stack.

we can find out that: The global execution context is created when the code starts to execute. There is one and only one, and it is always at the bottom of the execution context stack. It will be popped out when the browser window is closed.

When the function is called, the execution context of the function is created and pushed onto the stack.

Only the execution context at the top of the stack is active, that is, only the variable object at the top of the stack becomes the active object.

2.1 Variable Object (Variable Object, VO)

The variable object (VO) is an object similar to a container, which is closely related to the scope chain and execution context.

Three rules of the creation process of variable objects: Create arguments object. Check the parameters in the current execution context, and establish the attributes and attribute values ​​under the object.

Check the function declaration of the current execution context, that is, the function declared with the function keyword. Create an attribute with the function name in the variable object, and the attribute value is a reference to the memory address of the function. If the attribute already exists before, then the attribute will be overwritten by the new reference.

Check the variable declarations in the current execution context. Every time a variable declaration is found, an attribute is created in the variable object with the variable name, and the attribute value is undefined. If the attribute of the variable name already exists, in order to prevent the function with the same name from being modified to undefined, it will be shipped directly, and the original attribute value will not be modified.

The following pseudo-code can be used to represent variable objects:

VO={
Arguments: (), // actual parameters
Param_Variable: specific value, // formal parameter
Function:<function reference>,// Function reference
Variable:undefined//other variables
}

When the execution context enters the execution phase, the variable object will become an active object (AO). At this time, the originally declared variable will be assigned. Variable objects and active objects refer to the same object, but at different stages of the execution context.

We can represent the active object through the following pseudo-code:

AO={
Arguments: (),// actual parameters
Param_Variable: specific value,  // formal parameter
Function:<function reference>, // Function reference
Variable: Specific value  // Note, the value is already assigned here
}

Before entering the execution stage of the execution context, none of the properties in the variable object can be accessed. But after entering the execution phase, the variable object is transformed into an active object (activated), the properties inside can be accessed, and then the operation of the execution phase begins.

The variable object of global execution context The variable object of the global execution context is the window object, and this special is also applicable to this point, which also points to the window. In addition, the life cycle of the global execution context is consistent with the life cycle of the program. As long as the program does not end (such as closing the browser window), the global execution context will always exist. All other execution contexts can directly access the contents of the global execution context.

Look at a piece of code, pay attention to the comments

function func() {
console.log('function func');
}
var func = "coffe";
console.log(func); //>> coffe
// In the above code, according to three rules, the func of the variable declaration should be skipped when it  encounters the func of the function declaration.
// But why is the output result of func still being overwritten and displaying "coffe" in the end?
// That's because the three rules only apply to the creation phase of the variable object, that is, the creation phase of the execution context.
// And func="coffe" runs in the execution phase of the execution context, and the output result will naturally be "coffe".

This phenomenon is very confusing. In fact, it is also caused by the fact that the variable declared by var allows the same name. If the keyword let is used to declare the variable, this confusing situation can be avoided.

2.2 Scope Chain

definition The linked list composed of variable objects corresponding to multiple scopes in series is the scope chain. This linked list maintains access to the variable objects in the form of references. The scope chain guarantees orderly access to the variables and functions that meet the access permissions by the current execution context.

The top end of the scope chain must be the variable object corresponding to the current scope (local scope), and the bottom end must be the variable object corresponding to the global scope (global VO). The scope chain can be like a Steamer.

The bottom drawer is equivalent to the global scope. The steam in it (the visibility of variables and functions) can penetrate the entire steamer. The other drawers above the bottom are equivalent to the local scope, and the steam on these upper drawers can only affect The upper drawer.