We find that when we want to understand closures we have to explore the scope world, and when we want to understand this we have to look at it in conjunction with the lexical scope rules for a deeper understanding.

Total feeling between these knowledge points and points, as if there is a clear, unclear connection. In fact, the connection does end there. Closures and this can be understood in a complete knowledge link, which is the JS execution context.

We take the learning of JS execution context as an opportunity to string these points together and find deeper inner connections between them, so as to help us grasp the essence of JS this language more deeply.

In addition, from the point of view of the interviewer, the execution context and its related call stack level of knowledge, has been the JS language class examination questions inside the very deep and the bottom of the examination point. Hopefully, after reading this article, you’ll be able to answer any of these questions well.

Why have an execution context

When you usually write a project, you must write it file by file; Within each file, different methods and modules are broken down — I don’t think any student would cram thousands of lines of code logic into a single file. And when you do that, you’re actually practicing a very important idea in the software world — divide and conquer. (Just like the idea of merge sort)

Divide-and-conquer is a strategy for writing software in which you reduce the complexity of a large problem by breaking it into small, specific problems and solving them one by one. In the code, it is to break up the large logic into independent code blocks. These “code blocks” have different names depending on granularity, and can be functions, modules, packages, and so on.

To divide code logic into “chunks” is the wisdom of our programmers at the writing stage. It is the wisdom of the JS engine at the execution stage to divide the huge execution task into different execution contexts.

We can think of the execution context as the engine once again “partitioning” the code during execution, again for the purpose of breaking down complexity.

What is the execution context

Execution context, by definition, is “the environment in which code is executed” — a technical and abstract definition. From a learning perspective, I recommend that you understand the execution context in terms of its classification, composition, and lifecycle.

Classification of the execution context

Execution contexts fall into three main categories:

  • Global context – The context in which global code resides. Code that is not in a function is in the global execution context
  • Function context – The context created when a function is called
  • Eval execution context – The environment created when the code in the Eval function is run. Eval has been criticized by the front end for many years. All you need to do is say “Eval” and just skip it. (You can also do a favorability wave to show you’re a good kid who knows right from wrong.) In summary, Eval execution contexts are beyond the scope of this article.

Creation and composition of the global context

When our JS script runs, the first execution context created is the global context.

When we don’t have a single line of code in our script, the global context is cleaner, with only two things:

  • Global object (Window in browser, Global in Node)
  • This variable. This, again, refers to a global variable

But as soon as you write something in it, the world will come alive, for example, if you dare to write something like this:

var name = 'icon'
var tel = '123456'

function getMe() {
    return {
        name: name,
        tel: tel
    }
}
Copy the code

The composition of the global context is immediately enriched to look like this:

Phase: Creation

window: global object

this: window

name: undefined

tel: undefined

getMe: fn()
Copy the code

Name = ‘tel’; tel = ‘name’ This leads to a lifecycle of the context, and each execution context goes through a lifecycle like this:

  • Creation phase – The initialization state of the execution context, where not a single line of code has been executed but some preparatory work has been done
  • Execution phase – executes the code in the script line by line

Phase: Creation is the global context overview of the Creation Phase. Why does the variable have no value at this point? This is because during the creation phase, the JS engine only does a few things:

  • Create a Global object (Window/Global)
  • Create this and have it point to the global object
  • Allocate memory for variables and functions
  • The default variable is undefined; Put the function declaration into memory
  • Create scope chains

So far, the actual assignment has not been performed. So before you worry, let’s take a quick look at the global context of the execution phase:

Phase: Execution

window: global object

this: window

name: 'icon'

tel: '123456'

getMe: fn()
Copy the code

At this point we see that everything has values because the JS engine is already executing the code line by line and performing the assignment.

Note that the execution context is always dynamic during the execution phase. For example, if you execute the first line but do not execute the second line, then only name has a value and tel is undefined. One line down, tel also has a value, and you see that the contents of the execution context have changed again.

Understand the nature of variable promotion in context

Previously, people understood “variable enhancement”, perhaps more by memory. The book will tell you that in non-strict mode, something like this happens when we call a variable without declaring it:

/ There is no error, but outputundefined
console.log(name)

var name = 'icon'
Copy the code

Instead of throwing an undeclared error, the JS engine prints undefined, acting as if the name variable had already been declared. A phenomenon like this is called “variable lift”.

Now with our context creation process, you can see that there is no “lift” at all, and the variable stays in place. The so-called “promotion” is just an illusion caused by the fact that the variable creation process (completed in the context creation phase) is out of sync with the actual assignment process (completed in the context execution phase). The different work that the execution context does at different stages is the essence of variable promotion.

Creation and composition of a function context

If you fully understand the workflow of the global context above, the functional context should not be a problem for you. It is highly consistent with the global context at the mechanical level, and you only need to pay attention to the differences between it and the global context. The differences between the two are mainly reflected in the following aspects:

  • Creation timing – The global context is created as soon as the script is entered, while the function context is created when the function is called
  • Frequency of creation – the global context is created only once when the code is first interpreted; Function contexts, on the other hand, are determined by the number of function calls in the script and can theoretically be created an infinite number of times
  • The creation phase doesn’t work exactly the same — function contexts don’t create global objects (Windows), they create parameter objects (arguments); The created this no longer refers to the global object, but depends on how the function is called — if it is called by a reference object, this refers to the object; Otherwise, the value of this is set to global or undefined (in strict mode).

In addition, we can understand the functional context as well as the global context. Again, let’s use a simple example to see how a function context behaves at different stages:

var name = 'icon'
var tel = '123456'

function getMe() {
    return {
        name: name,
        tel: tel
    }
}

// Add function calls
getMe()
Copy the code

When the engine reaches the getMe () call line, it first enters the creation phase of the function context, where the function context reads as follows:

Phase: Creation

arguments: {length: 0}

this: window
Copy the code

It then enters the execution phase, executing the code inside the function line by line. Here we have only one line of code, and no variable changes are involved during code execution, so the content of the function context remains the same. After execution, the life cycle of the function context ends.

The call stack

We see that after the function completes its execution, its execution context disappears. This vanishing process is called “out of the stack” – yes, during the execution of the JS code, the engine creates an “execution context stack” (also called the call stack) for us.

Because there can be many function contexts, we cannot keep all of them. When a function completes execution, its corresponding context must relinquish the resources previously occupied. Context creation and destruction, therefore, correspond to an “on” and “off” operation. When we call a function, we push its context onto the call stack, exit the stack after execution, and then push the new function onto the stack.

Let’s take an example to understand this process:

function testA() {
  console.log('Execute the first test function logic');
  testB();
  console.log('Execute the logic of the first test function again');
}
function testB() {
  console.log('Logic for executing the second test function');
}
testA();
Copy the code

The execution context stack of the script above, as the code executes, goes through something like this:

  1. At the beginning of execution, the global context is created:

  1. At the testA call, the testA function context is created and the output is executedExecute the logic of the first test function

  1. A call to testB is encountered in testA, the corresponding function context is created, and the output is executedExecute the logic of the second test function

  1. When testB completes, the corresponding context is removed from the stack, leaving testA and the global context, and executing the outputExecute the logic of the first test function again

  1. After testA is executed, the corresponding execution context is removed from the stack, leaving the global context

Throughout this process, the call stack changes as shown below (from left to right) :

Understand the nature of scope from the perspective of the call stack

What is the scope? Previously, we thought of a scope as “a set of rules for accessing variables.” But now I’m going to tell you that the scope is really just the execution context that you’re in. Let’s understand the characteristics of scope based on the execution context:

Scopes are isolated

Let’s still use this code as an example:

function testA() {
  console.log('Execute the first test function logic');
  testB();
  console.log('Execute the logic of the first test function again');
}

function testB() {
  console.log('Logic for executing the second test function');
}

testA();
Copy the code

Here, the global scope is the outer scope as opposed to testA’s function scope; The global scope, the function scope of testA relative to testB, are all external scopes. As we know, in the case of nested scopes, the outer scope cannot access the variables of the inner scope. Now, with the call stack, you can see why:

In the case of testB, we see that when we are initially in the external scope (testA, global context), the corresponding context of testB has not yet been pushed onto the call stack. By the time testB execution ends and code execution falls back to the external scope, testB has already popped off the top of the stack. This means that each time testB is in an external scope, the execution context does not exist in the call stack at all. The testA function context and the global context cannot find any clues about testB’s internal variables.

Closures — Special “pops”

In general, once a function is off the stack, there is no way to access the variables inside the function. But closures are different:

function outer(a) {
  return function inner (b) {
    return a + b;
  };
}

var addA = outer(10);

addA(20) / / 30
Copy the code

In this case, the inner function references the outer function’s free variable A, forming a closure. Inner still has access to the a variable after outer has been removed from the stack — it doesn’t seem to disappear with outer’s execution context. Why?

Remember that the scope chain is also created during the creation of the execution context! The scope chain exists in the function as an internal property, to which the corresponding parent object is recorded when the function is defined. It is through this layer of scope chain relationships that closures preserve information about the execution context of the parent scope.

Look-up of free variables – a combination of scope chains and context variables

Earlier we talked about why it is difficult for an outer scope to “touch” an inner scope. On the other hand, while standing inside a function scope, you can access variables outside the scope. Why? Let’s change the code slightly:

var _name = 'icon'

function testA() {
  console.log('Execute the first test function logic');
  testB();
  console.log('Execute the logic of the first test function again');
}

function testB() {
  console.log(_name);
}

testA();
Copy the code

So let’s go back to testB. We see that when the code executes to testB, it is at the top of the call stack, with testA and the global context sitting firmly at the bottom of the call stack — creating the possibility for testB to find the free variable in the first place.

At execution time, if, like testB in this example, the _name variable is not found inside the function scope, the engine will find the target variable up the scope chain to see if it exists in the context of the parent scope. Move up the scope chain until you find the Window (bottom level). If the window does not have _name, then JS will throw Uncaught ReferenceError: _name is not defined

Attention! This is a chain of scopes, not a stack of calls. The call stack is formed during execution, whereas the scope chain is determined at writing time. Therefore, variables that cannot be found in testB are never found in testA, but in global context variables!