There are three main execution contexts in JavaScript: global execution context, function execution context, and eval execution context.

The execution context includes the variable environment, lexical environment, external environment, and this.

scope

JavaScript engines implement function-level scope through variable environments and block-level scope on top of function scope through lexical environments.

Take a look at this code:

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()
Copy the code

Next, we will analyze the execution process of the above code step by step.

The first step is to compile and create the execution context. Here is a schematic of the execution context:

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

  • All variables declared by var inside the function are stored in the variable environment at compile time.
  • Variables declared by let are saved to the ** Lexical Environment ** at compile time.
  • Inside the scope of the function, variables declared by let are not stored in the lexical environment.

The second step is to continue executing the code. When it is executed inside 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 follows:

Can be seen from the diagram, when entering the function block, the scope of the scope block through the let statement variables that 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 block b also declares the variables, When executed inside the scope, they all exist independently.

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 scoped block, the variables inside the scoped 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. Note that variables are declared by let or const.

Next, when the console.log(a) line is executed in the scope block, we need to find the value of variable A in the lexical environment and variable environment by: Search down the stack of the lexical environment. If it finds something in a block in the lexical environment, it returns it directly to the JavaScript engine. If it doesn’t find something, it continues in the variable environment.

This completes the variable lookup process, as you can see below:

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:

So, block-level scope is implemented through the stack structure of the lexical environment, and variable promotion is implemented through the variable environment. By combining the two, the JavaScript engine supports both variable promotion and block-level scope.

The scope chain

The variable environment of each execution context contains an external reference to the execution context. We call this external reference outer, which is specified by the lexical scope.

Let’s start with the following code:

function bar() {
  console.log(myName)
}
function foo() {
  var myName = Geek Bang
  bar()
}
var myName = "Geek Time"
foo()
Copy the code

When a piece of code uses a variable, the JavaScript engine first looks for the variable in the current execution context. For example, if the myName variable is not found in the current context, The JavaScript engine then continues to look in the execution context that Outer points to. To get an idea, take a look at this:

As you can see from the figure, the outer of both bar and foo points to the global context, which means that if an external variable is used in bar or foo, the JavaScript engine will look in the global execution context. We call this chain of lookup the scope chain.

Now that you know that variables are looked up through the scope chain, one question remains: why is the external reference to the bar function called by foo the global execution context, and not foo’s execution context?

This is because during JavaScript execution, its scope chain is determined by lexical scope.

Lexical scope

Lexical scope means that the scope is determined by the position of the function declaration in the code, so lexical scope is a static scope that predicts how the code will look up identifiers during execution.

That is, the lexical scope is determined at the code stage, regardless of how the function is called.

Combining the variable environment, lexical environment, and scope chain, let’s look at the following code:

function bar() {
  var myName = "Geek World."
  let test1 = 100
  if (1) {
    let myName = "Chrome"
    console.log(test)
  }
}
function foo() {
  var myName = Geek Bang
  let test = 2
  {
    let test = 3
    bar()
  }
}
var myName = "Geek Time"
let myAge = 10
let test = 1
foo()
Copy the code

For the above code, when executing into the if block inside the bar function, the call stack looks like this:

Explain the process. The first step is to look in the execution context of the bar function, but since the test variable is not defined in the execution context of the bar function, according to the rules of lexical scope, the next step is to look in the outer scope of the bar function, that is, the global scope.

this

This is bound to the execution context, meaning that there is a this in each execution context.

Take a look at this code:

function foo(){
  console.log(this)
}
foo()
Copy the code

We print this inside foo, execute this code, and print out the window object as well, indicating that by default we call a function whose execution context this also refers to the window object.

There are generally three ways to change this in the execution context:

1. Use the call/apply/bind method of the function

let bar = {
  myName : Geek Bang.test1 : 1
}
function foo(){
  this.myName = "Geek Time"
}
foo.call(bar)
console.log(bar)
console.log(myName)
Copy the code

By executing this code and looking at the output, you can see that this inside foo already refers to the bar object, because by printing the bar object, you can see that the bar myName property has changed from “geek state” to “geek time.” Also print myName in the global execution context, and the JavaScript engine tells you that the variable is undefined.

2. Call method Settings through the object

Take a look at this code:

var myObj = {
  name : "Geek Time".showThis: function(){
    console.log(this)
  }
}
myObj.showThis()
Copy the code

In this code, we define a myObj object consisting of a name attribute and a showThis method, and then call the showThis method from the myObj object. By executing this code, you can see that the final output value of this refers to myObj.

So, you can conclude that you use an object to call a method inside it whose this refers to the object itself.

In fact, you can also think of the JavaScript engine executing myObject.showthis () as converting it to:

myObj.showThis.call(myObj)
Copy the code

Next we change the callback slightly by assigning showThis to a global object and then calling it as follows:

var myObj = {
  name : "Geek Time".showThis: function(){
    this.name = Geek Bang
    console.log(this)}}var foo = myObj.showThis
foo()
Copy the code

When you execute this code, you’ll see that this again refers to the global Window object.

So by comparing the above two examples, you can draw the following two conclusions:

  • Call a function in the global environment. This inside the function refers to the global variable Window.
  • Call a method within an object whose execution context this refers to the object itself.

3. Set it in the constructor

You can set this in the constructor like this, with the following example code:

function CreateObj(){
  this.name = "Geek Time"
}
var myObj = new CreateObj()
Copy the code

In this code, we create the object myObj using new. Do you know who this in CreateObj refers to?

In fact, when new CreateObj() is executed, the JavaScript engine does four things:

  • First we create an empty object tempObj;
  • Call the createobj. call method with tempObj as an argument to the call method, so that when CreateObj’s execution context is created, its this refers to the tempObj object.
  • The CreateObj function is then executed. This in the execution context of CreateObj refers to the tempObj object.
  • Finally, the tempObj object is returned.

To get the intuition, we can use code to demonstrate:

var tempObj = {}
CreateObj.call(tempObj)
return tempObj
Copy the code

Thus, we build a new object with the new keyword, and this in the constructor is the new object itself.

Design defects of this and solutions

1. This in nested functions does not inherit from outer functions

Let’s combine the following code to analyze:

var myObj = {
  name : "Geek Time".showThis: function(){
    console.log(this)
    function bar(){
      console.log(this)
    }
    bar()
  }
}
myObj.showThis()
Copy the code

If you’re new to JavaScript, it might seem intuitive that this in bar should be the same as this in its outer showThis function, pointing to myObj objects. After executing this code, you will see that this in bar refers to the global Window object, while this in showThis refers to myObj. This is one of the most confusing parts of JavaScript, and a source of many problems.

  • Option 1: Declare a variable self to hold this
var myObj = {
  name : "Geek Time".showThis: function(){
    console.log(this)
    var self = this
    function bar(){
      self.name = Geek Bang
    }
    bar()
  }
}
myObj.showThis()
console.log(myObj.name) / / geek
console.log(window.name) // 
Copy the code

When you execute this code, you can see that it outputs the desired result, eventually changing the value of the name property in myObj to “Geekbang.” In fact, the essence of this method is to transform this schema into a scoped schema.

  • Scheme 2: ES6 arrow function
var myObj = {
  name : "Geek Time".showThis: function(){
    console.log(this)
    var bar = () = >{
      this.name = Geek Bang
      console.log(this)
    }
    bar()
  }
}
myObj.showThis()
console.log(myObj.name)
console.log(window.name)
Copy the code

When you execute this code, you’ll see that it also outputs the desired result, which is that the this in the arrow function bar refers to the myObj object.

This is because the arrow function in ES6 does not create its own execution context, so the this in the arrow function depends on its external function.

2. By default, this in normal functions refers to the global object Window

Call a function whose execution context this refers by default to the global object Window.

However, this design is also a flaw, because in practice we don’t want this in the execution context of a function to point to a global object by default, as this would break the boundaries of the data and cause some misoperations. If you want this in the execution context of a function to point to an object, the best way to display the call is through the call method.

This problem can be solved by setting JavaScript to “strict mode.” In strict mode, the default execution of a function whose execution context this is undefined solves the above problem.