preface

The JavaScript execution process is divided into two phases: compilation phase and execution phase. In the compilation stage, JS engine mainly does three things: lexical analysis, syntax analysis and code generation; Once compiled, the JS engine creates the execution context (the environment in which the JavaScript code runs) and executes the JS code.

Compilation phase

For common compiled languages (such as Java), the compilation steps are: lexical analysis -> parsing -> semantic checking -> code optimization and bytecode generation

For interpreted languages (such as JavaScript), the compile level interprets and executes code through lexical analysis -> parsing -> code generation.

Lexical analysis

The JS engine will decompose the code we write as a string into lexical units (tokens). For example, var a = 2, the program is split into: “var, a, =, 2,;” Five token. Each lexical unit token is indivisible. You can try this website address to view tokens: esprima.org/demo/parse….

Syntax analysis

The parsing phase transforms a stream of lexical units (arrays), known as tokens, into a tree-like “Abstract Syntax tree (AST)”

Code generation

The process of converting an AST into executable code is called code generation, because the computer can only recognize machine instructions and needs some way to change var a = 2; The AST of a is converted into a set of machine instructions that create variables of A (including allocating memory) and store values in A.

Execution phase

Just as executing programs requires an execution environment, Java requires a Java virtual machine, and parsing JavaScript requires an execution environment, which we call “execution context.”

What is an execution context

In short, an execution context is an abstraction of the execution environment of JavaScript code, and whenever JavaScript runs, it runs in the execution context.

Execution context type

There are three types of JavaScript execution contexts:

  • Global execution context – When the JS engine executes global code, it compiles the global code and creates the execution context. It does two things: 1) create a global window object (in the browser environment), 2) set this to the global object; The global context is valid for the entire page life cycle and has only one copy.

  • Function execution context – When a function is called, the code inside the function is compiled and the function execution context is created. Generally, the function execution context is destroyed after the function is finished.

  • Eval Execution Context — Calling the eval function also creates its own execution context (eval is prone to malicious attacks and runs code slower than the corresponding alternatives, so it is not recommended)

Execution stack

The concept of execution stack is relatively close to our programmers, learning it can let us understand the principle of JS engine behind the work, to help us debug the code in the development, but also to deal with the interview questions about execution stack.

The execution stack, known in other programming languages as the “call stack,” is a data structure for LIFO (LIFO) stacks that are used to store all execution contexts created while the code is running.

When the JS engine starts executing the first line of JavaScript code, it creates a global execution context and pushes it onto the execution stack. Each time the engine encounters a function call, it creates a new execution context for that function and pushes it onto the top of the stack.

The engine executes functions whose execution context is at the top of the stack. When the function completes execution, the execution context pops out of the stack and the control flow moves to the next context in the current stack.

Consider the following code:

let a = 'Hello World! ';
function first() {
  console.log('Inside first function');
  second();
  console.log('Again inside first function');
}
function second() {
  console.log('Inside second function');
}
first();
console.log('Inside Global Execution Context');
Copy the code

When the above code loads in the browser, the JS engine creates a global execution context and pushes it onto the current execution stack. When first() is encountered, the JS engine creates a new execution context for the function and pushes it to the top of the current execution stack.

When called from within the first() function, the second() JS engine creates a new execution context for the second() function and pushes it to the top of the current execution stack. When second() completes, its execution context is popped off the current stack and the control flow moves to the next execution context, that of the first() function.

When first() completes, its execution context pops off the stack and the control flow reaches the global execution context. Once all code has been executed, the JavaScript engine removes the global execution context from the current stack.

How do I create an execution context

Now that we have seen how the JS engine manages execution contexts, how are execution contexts created?

The creation of an execution context is divided into two phases:

  • Creation stage;
  • Execution stage;

Create a stage

The execution context creation phase does three things:

  • The bindingthis
  • Create a lexical environment
  • Create variable environment

So the execution context is conceptually expressed as follows:

ExecutionContext = { // Execution context
  Binding This, // This value binding
  LexicalEnvironment = { ... }, // lexical context
  VariableEnvironment = { ... }, // Variable environment
}
Copy the code
Binding this

In the context of global execution, the value of this points to a global object. In browsers, this refers to the Window object.

In the context of function execution, the value of this depends on how the function is called

  • Calling a function through an object method,thisPoint to the invoked object
  • After declaring a function, use the function name to make a normal call.thisPoints to global objects, in strict modethisThe value isundefined
  • usenewMethod to call a function,thisPoints to the newly created object
  • usecall,apply,bindHow to call a function will changethisTo the first argument passed in, for example

function fn () {
  console.log(this)}function fn1 () {
  'use strict'
  console.log(this)
}

fn() // A normal function call. This refers to the window object
fn() // In strict mode, this is undefined

let foo = {
  baz: function() {
  console.log(this);
  }
}

foo.baz();   // 'this' points to 'foo'

let bar = foo.baz;

bar();       // 'this' points to the global window object because no reference object is specified

let obj {
  name: 'hello'
}

foo.baz.call(obj) // call changes this to point to the obj object
Copy the code
Lexical environment

Each lexical environment consists of two parts:

  • Environment record: Variable object = stores declared variables and functions (let, const, function, function arguments)
  • External environment references: scope chains

The official ES6 documentation defines the lexical environment as:

Lexical Environments are a canonical type used to define the associations of identifiers with specific variables and functions based on the Lexical nesting structure of ECMAScript code. The Lexical Environment consists of an Environment Record and a potentially empty outer Lexical Environment reference.

In simple terms, a lexical environment is a structure of identifiers — variables mapping (identifiers are the names of variables/functions, and variables are references to actual objects [objects containing functions and array types] or underlying data types).

As an example, look at the following code:

var a = 20;
var b = 40;
function foo() {
  console.log('bar');
}
Copy the code

The lexical environment for the code above looks like this:

lexicalEnvironment = {
  a: 20.b: 40.foo: <ref. to foo function>
}
Copy the code

Environmental records

The context record is the place in the lexical context where variable and function declarations are recorded

There are also two types of environmental records:

Declare class environment records. As the name implies, it stores variable and function declarations, and the lexical context of a function contains a declarative class environment record.

Object environment record. The lexical environment in the global environment contains an object environment record. In addition to variable and function declarations, object environment records also include global objects (the browser’s Window object). Therefore, a new entry is created in the record for each new property of the object (which in the case of the browser contains all properties and methods that the browser provides to the Window object).

Note: For functions, the environment record also contains a Arguments object, which is a class array object containing the parameter index and parameter mapping, and a length attribute for the parameters passed to the function. For example, a Arguments object looks like this:

function foo(a, b) {
  var c = a + b;
}
foo(2.3);
The argument object looks something like this
Arguments: { 0: 2.1: 3.length: 2 }
Copy the code

Environment record objects are also called variable objects (VO) in the creation phase and active objects (AO) in the execution phase. It is called a variable object because it simply stores the variable and function declarations in the execution context. After the code is executed, the variable is gradually initialized or modified, and then the object is called an active object

External environment reference

A reference to an external context means that the external lexical context is accessible in the current execution context. That is, if a variable is not found in the current lexical context, the Javascript engine will try to find it in the upper-level lexical context. (Javascript engines use this property to form what we call a scope chain.)

The lexical environment is abstracted to look like the following pseudocode:

GlobalExectionContext = { // Global execution context
  this: <global object> // this value binds LexicalEnvironment: {// global execution context lexical EnvironmentRecord: {// EnvironmentRecord Type: } outer: <null> // External reference}} FunctionExectionContext = {// Function execution context this: <depends on how function is called> // this value binding LexicalEnvironment: {// function execution context lexical EnvironmentRecord: {// EnvironmentRecord Type: } outer: <Global or outer function environment reference> //Copy the code
The variable environment

It is also a lexical environment whose environment logger holds bindings created in the execution context of variable declaration statements.

As mentioned above, the variable environment is also a lexical environment, so it has all the attributes of the lexical environment defined above.

In ES6, one difference between lexical and variable environments is that the former is used to store function declarations and variable (let and const) bindings, while the latter is used only to store var variable bindings.

Look at the sample code to understand the above concepts:

let a = 20;
const b = 30;
var c;

function multiply(e, f) {
 var g = 20;
 return e * f * g;
}

c = multiply(20.30);
Copy the code

The execution looks like this:

GlobalExectionContext = {

  ThisBinding: <Global Object>, LexicalEnvironment: {EnvironmentRecord: {Type: "Object", < uninitialized >, multiply: < func > } outer: <null> }, VariableEnvironment: { EnvironmentRecord: { Type: } outer: <null>}} FunctionExectionContext = {ThisBinding: <Global Object>, LexicalEnvironment: {EnvironmentRecord: {Type: "Declarative", // 30, length: 2}, }, outer: <GlobalLexicalEnvironment> }, VariableEnvironment: { EnvironmentRecord: { Type: G: undefined, outer: <GlobalLexicalEnvironment>}}Copy the code

Note – The function execution context is created only if the multiply function is called.

You may have noticed that the let and const variables are not associated with any value, but the var variable is set to undefined.

This is because during the creation phase, the engine examines the code for variable and function declarations, which are stored entirely in the environment but are initially set to undefined (in the case of var) or uninitialized (in the case of let and const).

This is why you can access variables defined by var (albeit undefined) before declaration, but access let and const variables before declaration will get a reference error.

This is what we call variable declaration enhancement.

Execution phase

After creating the execution context above, you are ready to execute the JavaScript code. At execution time, if the JavaScript engine cannot find the value of the let variable at the actual location declared in the source code, it is assigned the value of undefined.

Execution stack application

Use the browser to view the stack call information

We know that the execution stack is the data structure used to manage the invocation relationship of the execution context, so how do we use it in practice?

The answer is that we can use the browser “developer tools” source TAB, select the JavaScript code to hit the breakpoint, you can see the function call relationship, and can switch to see the variable value of each function

If we create a breakpoint inside second, we can see that the Call Stack on the right shows the relationship between the second, first, and (anonymous) calls. Second is at the top of the Stack. By executing the second function, we can check the values of local variables A, B and num in the Scope of the function. By checking the call relation of the call stack, we can quickly locate the status of our code execution.

So if the code execution error, also do not know where to interrupt the debugging point, how to check the call stack where the error, tell you a trick, as shown in the following figure

We do not need to break the point, the above two steps, we can automatically break the code where the execution of the exception. Once you know this technique, you don’t have to worry about code errors anymore.

In addition to viewing the call stack through the breakpoint above, you can also use console.trace() to print the current function call relationship. For example, if you add console.trace() to the second function in the sample code, you can see the result of the console output as shown below:

conclusion

JavaScript execution is divided into two phases, compilation and execution. The compilation stage will generate executable code through lexical analysis, syntax analysis and code generation steps. Executable code executed by the JS engine creates execution contexts, including binding this, creating lexical and variable environments; The lexical environment creates external references (scope chains) and the record environment (variable objects, lets, const, function, arguments), and the JS engine creates execution up and down after completion and starts single-threaded JS code execution from top to bottom line.

Finally, some tips on how to use the call stack in the development process are shared.

Reference links

JavaScript parsing, AST, V8, JIT

Understand the execution context and execution stack in JavaScript

Understand the execution context and execution stack in Javascript

Recommended reading

  • Learn more about scopes and closures – All in all (JS Series 2)
  • The Use and Pitfalls of this that a qualified front-end person must know (JS Series 3)