When the JAVASCRIPT engine processes a piece of script content, in what order is it parsed and executed? When are those variables defined in the script? How are the intricate access relationships between them created and linked?

Execution context

The JavaScript standard defines all information required to execute a piece of code (including a function) as an “execution context.”

Because this part of the term has gone through many versions and community interpretations, the definition is confusing and has different version definitions.

Execution context in ES3

There are three types of execution context in JavaScript:

  • Global execution context
    • The most basic execution context,There is only one global execution context in a program, and in the wholeJavaScriptThe script lives at the bottom of the execution stack and is not destroyed by the stack.
  • Function execution context
    • Each time a function is called, a new function execution context is created (whether or not the function is called repeatedly).
  • The Eval function executes the context
    • Code executed inside the eval function also has its own execution context.

Variable Object

A variable object (VO for short) is an object representing a variable that is present in every execution context.

  • In the context of global execution,A global object is generatedIn the case of the browser environment, the global object iswindow), and willthisThe value is bound to this global object.
  • Function execution context,When a function is called and before the actual function code is run, the JS engine willWith the argument list of the current function (argumentsInitializes a “variable object” and associates the current execution context with itDeclared in the function code blockvariableandfunctionWill be added to the variable object as a property.

There are a few points to note here:

  1. Only function declarations are added to variable objects, and function expressions are ignored.
// Function declaration
function a () {}
console.log(a); / / ƒ (a) {}

// Function expression
var b = function _b () {}
console.log(b); / / ƒ (b) {}
console.log(_b); // Uncaught ReferenceError: _b is not defined
Copy the code
  1. Attributes defined internally by a variable object are not directly accessible. Properties and methods are only accessible when the variable object is activated as an activation object (AO) while the function is running.

In fact, variable objects and active objects are the same thing, but in different states and stages.

Scope (scope)

Scopes, also known as scope chains, specify how variables are found, that is, how the currently executing code has access to the variables.

When a variable is queried, it is first looked up in the variable object of the current execution context. If not, it looks in the variable object of the parent execution context until it looks in the global object. Thus a linked list of variable objects in multiple execution contexts is called a scoped chain.

Since the scope is made up of variable objects, the scope of a function is determined when the function is created. When a function is created, there is an internal property called [[scope]] to which all parent variable objects are stored. When the function is executed, the scope chain of the execution environment is constructed by copying the objects in the [[scope]] property of the function. VO is then activated to generate AO and added to the front of the scope chain. The complete scope chain is created:

scope = [AO].concat([[scope]]);
Copy the code

This value (this value)

The this value defaults to a global object. When a function is called as an object property, or when the apply, call, or bind methods are used, or when the arrow function is used, the this value is not a global object, the logic of which will be described later.

The lifecycle of an execution context in ES3

  1. The creation phase occurs before the function is called and executed.
    • With the argument list of the current function (argumentsInitializes a “variable object” and associates the current execution context with it, and is declared in the function code blockvariableandfunctionWill be added to this variable object as an attribute (i.eVariable declaration promotion).
    • Build the scope chain
    • Set up thethisvalue
  2. Execution phase
    • The code executes line by line, assigning values to defined variables, accessing variables along the chain of job domains, creating a new execution context if there are internal function calls, pushing it onto the execution stack, and handing over control.
  3. Destruction of phase
    • When the function completes, the current execution context is ejected from the execution context stack and destroyed, and control is returned to the execution context one layer above the execution stack.

Execution context in ES5

ES5 modifies some of the concepts in ES3, removing the variable objects, active objects and scopes, and changing them into lexical environment and variable environment.

Grammar environment

ES6 official lexical environment definition:

A lexical environment is a canonical type that defines the associations of identifiers and concrete variables and functions based on the lexical nesting structure of ECMAScript code. A lexical environment consists of an environment logger and a null value that may reference an external lexical environment. In general, lexical environments are associated with specific ECMAScript code syntactic structures, such as functions, code blocks, Catch clauses in TryCatch, and a new lexical environment is created each time such code is executed.

In short, a lexical environment is a structure that holds identifiers – variables. An identifier here refers to the name of a variable/function, and a variable is a reference to an actual object (containing function-type objects) or to raw data.

The syntax environment is divided into two parts, which can be explained in this article:

  • Environment Record: An identifier binding that records the corresponding code block, which can be understood as variable objects and active objects in ES3.
  • References to external lexical environments (OUTER) : Used to form logically nested structures of multiple lexical environments to achieve the ability to access external lexical environment variables. Think of it as a scope chain in ES3.

The variable environment

The variable environment is also a lexical environment, so it has all the characteristics of a lexical environment.

A variable environment is created for block-level scope in ES6 (through new commands such as let and const).

  • Variable environment for recordingvarDeclared bindings.
  • Lexical environment for recording other declared bindings (e.glet,const,class, etc.).

The lifecycle of an execution context in ES5

The cycle is the same as ES3, except that it is different in the creation phase:

  1. That creates the execution contextLexical environment
    • createDeclarative environment logger, store variables, functions, and parameters (responsible for processingletandconstDefined variables)
    • Create an external environment reference with a value of global object or parent lexical environment (coscoped)
  2. That creates the execution contextThe variable environment
    • createDeclarative environment logger, store variables, functions, and parameters (responsible for processingvarA variable defined with an initial value of undefined causes the declaration to be raised.
    • Create an external environment reference with a value of global object or parent lexical environment (coscoped)
  3. determinethisvalue

Execution context in ES9

  • Lexical environment: lexical environment, used when fetching variables or this values.
  • Variable environment: used when declaring variables.
  • Code Evaluation State: Used to restore the code execution location.
  • Function: used when the task being executed is a Function.
  • ScriptOrModule: used when executing a ScriptOrModule, indicating the code being executed.
  • Realm: Base libraries and built-in object instances to use.
  • Generator: Only the Generator context has this property, indicating the current Generator.

Note: specific details will be summarized later, now make a record ~

closure

Closure definition: a function that has access to the internal variables of another function. Simply put, a function is called a closure if it is referred to externally as the return value of another function.

The concept of closures first appeared in The Computer Journal in 1964, P. J. Landin put forward The concepts of Applicative Expression and closure in The Mechanical Evaluation of Expressions.

In this classical definition of a closure, there are two parts to a closure. 1. Environment 1.1 Environment 1.2 Identifier List 2. Expression section

Based on this classical definition, we find the components of closures in JavaScript.

  1. Context 1.1 Context: the lexical context of a function (part of the execution context) 1.2 Identifier List: undeclared variables used in a function 2. Expression part: function body

Here we see that the JavaScript equivalent of a closure is a “function.”

A memory leak

When the parent function contained in the closure completes execution, its scope chain is destroyed, but the variable object in the parent function remains in memory because of the closure. The closure is freed from memory only when it is destroyed. Therefore, there is a risk of memory leaks when closures are overused.

This value

The this value is designed to refer to the current operating environment of the function within the function body.

The value of this in a function is determined when the function is actually called and executed. When a function is defined, it is impossible to determine the value this points to, because the value of this is part of the execution context. Each time a function is called, a new execution context environment will be generated.

The global environment

In a global environment, the this value represents a global object.

this= = =window // true

function f() {
  console.log(this= = =window);
}
f() // true
Copy the code

The constructor

In the constructor, this refers to the instance object.

var Obj = function (p) {
  this.p = p;
};

var o = new Obj('Hello World! ');
o.p // "Hello World!"
Copy the code

Since this refers to the instance object, defining this.p inside the constructor is equivalent to defining the instance object to have a p attribute.

Object methods

If the object’s method contains this, this points to the object on which the method is run. This method assigns to another object, which changes the reference to this.

var obj ={
  foo: function () {
    console.log(this); }}; obj.foo()// obj
(false || obj.foo)() // window
Copy the code

If the method is not in the first layer of the object, this refers only to the current layer of the object and does not inherit from the layer above.

var a = {
  p: 'Hello'.b: {
    m: function() {
      console.log(this.p); }}}; a.b.m()// undefined
Copy the code

Bind this method

Call and apply

The call/apply method of a function instance can specify the point of this inside the function (that is, the scope in which the function is executed), and then call the function within the specified scope.

var obj = {};

var f = function () {
  return this;
};

f() === window // true
f.call(obj) === obj // true
f.apply(obj) === obj // true
Copy the code

bind

The bind() method is used to bind this in the function body to an object and return a new function.

var obj = {};

var f = function () {
  return this;
};

f() === window // true
f.bind(obj)() === obj // true
Copy the code

Arrow function

Arrow functions do not have the this binding, which is determined by the nearest layer of non-arrow functions (the value of this for non-arrow functions is obtained by the above logic), that is, by querying the scope chain to determine its value.

var f = (a)= > {
    console.log(this= = =window);
}
f(); // true

var obj = {
    name: 'Hello World! '.f: (a)= > {
        console.log(this= = =window);
    },
    s() {
      setTimeout((a)= > {
        console.log(this.name);
      }, 100); }}var newObj = {
    name: 'Hello! ',
}

obj.f(); // true
obj.s(); // Hello World!
obj.s.call(newObj); // Hello!
Copy the code

Execution context stack

As you can see, multiple execution contexts may arise when a program is running. To manage these Execution contexts, the JavaScript engine creates a LIFO stack (LIFO)– an Execution Context stack (ECS).

Let’s use this GIF to understand the execution context stack’s execution logic:

  1. Before starting any JavaScript code execution, the global context is created and pushed, so it remains at the bottom of the stack.
  2. Each call to the function creates a new execution context (even if it calls itself from within the function) and pushes it onto the stack.
  3. The function returns after execution, and its execution context is removed from the stack.
  4. After all code runs, the execution context stack is left with the global execution context.

Stack overflow and tail call optimization

As we have seen above, functions are executed in isolation from the execution context stack. However, the execution stack itself has capacity limits. A stack overflow error occurs when the execution context object inside the execution stack is overstocked to a certain extent.

This happens most often in recursive calls:

Num = 1, num = 1, num = 1, num = 1
function recursion (num) {
    if (num === 0) return num;
    return recursion(num - 1) + num;
}

recursion(100) / / = > 5050
recursion(1000) / / = > 500500
recursion(10000) / / = > 50005000
recursion(100000) // => Uncaught RangeError: Maximum call stack size exceeded
Copy the code

To solve this problem, we can use tail-call optimization.

A tail call is when a function is called as the last statement of another function. In the above recursion, the execution of each function is stored in the execution context stack, but if the last call satisfies one of the following points, it does not add data to the context stack, but optimizes, clears and reuses the current stack:

  1. Tail calls do not access variables on the current stack (i.e. tail calls are not closures)
  2. Inside a function, the tail call is the last statement
  3. The result of the last call is returned as a function value
// Tail call optimization
function recursion (num, sum = 0) {
    "use strict";
    if (num === 0) return sum;
    return recursion(num - 1, sum + num);
}
Copy the code

Tail-recursive optimization is something that no browser currently supports (Safari 13 reportedly does), nor does Babel compilation.

Note: tail-call optimization is not well supported in browsers, so let’s take a look at the concept here.

reference

  • Interviewer: Tell me about the execution context
  • An in-depth JavaScript series ii: Execution Context
  • JavaScript Execution (2) : What are closures and execution contexts?
  • This keyword