The order in which JavaScript is executed

For human intuition, a string of JavaScript code execution logic should be executed line by line, so is JavaScript execution line by line? Let’s start with an example:

function fn() {
  console.log('fn1');
}
fn(); // fn2

function fn() {
  console.log('fn2');
}
fn(); // fn2
Copy the code

The result of both calls is fn2. Why is this so? Anyway, here’s another example:

showName();
console.log(myName); // undefined
var myName = "Jackson";

function showName() {
  console.log("ShowName function executed"); // the showName function is executed
}
Copy the code

If the JavaScript engine executes the code line by line, the first line of code will report an error, but the showName function is actually called. The second line of code does not declare the myName variable, but prints undefined, indicating that myName is declared and undefined. This shows that the JavaScript engine does not execute the code line by line. How does a JavaScript engine execute a piece of code?

Variable ascension

Before you can understand how JavaScript code executes, you need to understand what variable promotion is. Let’s start with JavaScript declarations and assignments.

Variable declaration and assignment:

function fn() {
  console.log(myName);
}
var myName = "Jackson";
Copy the code

The above code can be split into two parts, declare and copy, function because there is no assignment operation, is the complete declaration, as follows:

var myName = undefined; / / declare
function fn() {
  console.log(myName);
}
myName = "Jackson"; / / assignment
Copy the code

Now that we’ve learned about declarations and assignments, let’s look at variable promotion.

Variable promotion refers to that during the execution of JavaScript, the JavaScript engine will promote the variable declaration and function declaration to the beginning of the code. After the variable promotion, the default value will be undefined.

To combine the concept of variable promotion, simulate the following code:

showName();
console.log(myname)
var myName = "Jackson";

function showName() {
  console.log("ShowName function executed");
}

/** The variable promotion part */
var myName = undefined;
function showName() {
  console.log('showName function executed ');
}

/** Executables */
showName();
console.log(myname)
myName = 'Jackson';

Copy the code

Through this code simulation, we know that functions and variables have been promoted to the beginning of the code before executing it. The promotion of variables means that the code is moved from the physical level to the front. As simulated above, variables and functions do not change position during execution, but are put into memory by the JavaScript engine at compile time. So the general process of executing a section of JavaScript is as follows:

  1. Compilation stage: variable promotion operations are performed on variables and functions as follows:
/** The variable promotion part */
var myName = undefined;
function showName() {
  console.log('showName function executed ');
}
Copy the code
  1. Code execution phase: Code execution line by line
/** Executables */
showName();
console.log(myname)
myName = 'Jackson';
Copy the code

JavaScript code is compiled into two parts: the execution context and the executable code. Execution context includes: global execution context, function execution context, eval execution context;

An execution context is the environment in which JavaScript executes a piece of code. See the following figure for details:

The Viriable Environment in the execution context holds the contents of the variable promotion, such as myName and showName functions. The execution context also has other objects, such as the block-level scope variables in the lexical Environment. Each execution context stores an external reference, outer, to the external environment; And each execution context holds a this. We’ll talk more about that later, but I won’t go into that here.

Objects stored in a variable environment can be expressed simply as follows:

ViriableEnvironment:
  myName -> undefined,
  showName ->function : {console.log(myname)
Copy the code

In the execution phase, the JavaScript engine executes the executable code sequentially, line by line. Display assignment to declared myName, then execute showName, variable environment as follows:

ViriableEnvironment:
  myName -> 'Jackson',
  showName ->function : {console.log(myname)
Copy the code

So far, you’ve seen how JavaScript executes code. First, the JavaScript engine compiles the code, and then there’s the variable promotion part, which is stored in the variable environment, and then there’s the code execution part. So how does JavaScript execute if you have two identical variables and functions? With the first example in this article, the later code overrides the previous code.

The call stack

As mentioned above, there are three cases in which a piece of code is compiled and an execution context is created:

  1. Global execution context: When JavaScript global code is executed, the global code is compiled to create a global execution context. There is only one global execution context for the lifetime of the page.
  2. Function execution context: When a function is called, the internal code of the function is compiled and the execution context of the function is created. After the function is executed, the created execution context of the function is destroyed.
  3. When the eval function is used, the eval code is compiled and an EVAL execution context is created.

We have identified three execution contexts and talked about what a call stack is. A call stack is a stack (lifO) data structure. A call stack is a data structure used to manage the relationship between function calls. In the following JavaScript code, how is the execution context created and stored?

var a = 1;
function fn1(b, c) {
  var a = 5;
  console.log(a);
  return a + b + c;
}
function fn2(b, c) {
  var d = 10;
  res = fn1(b, c);

  return res + d;
}

fn2(2.3);
Copy the code

The above code declares a variable a, fn1, fn2, call fn2 in the global scope, fn1 in fn2, how is the call stack executed during the entire code execution? The following is a step by step analysis of how the call stack changes.

  1. Create the global context and push it to the bottom of the stack, as shown:

As you can see from the figure, variable A, functions fn1, and fn2 are stored in the variable environment object of the global context. It is important to note that function calls are accompanied by value passing. After the global execution context is pushed, JavaScript starts executing the global code, starting with an assignment of a = 2.

  1. By calling the fn2 method, the JavaScript engine compiles the function, creates the execution context, and pushes it onto the call stack, as shown in the figure below

After the execution context of function fn2, the code in function fn2 is executed, assigning 10 of fn2 to d, that is, d = 10;

  1. By calling the fn1 method, the JavaScript engine compiles the function, creates the execution context, and pushes it onto the call stack, as shown below

After the execution context of the function fn1 is pushed, the code in fn1 is executed and the value of 5 in Fn1 is assigned to A, that is, a = 5; And executes a + b + C to return to the res caller in the fn2 function.

  1. After the fn1 execution is completed, the fN1 execution context is removed from the stack, as shown in the figure

After fn1 is executed, the return value is assigned to res in fn2, that is, res = 10

  1. After the fn2 execution is complete, the fn2 execution context is removed from the stack, as shown in the figure

After fn2 is executed, the value res + d is returned. The global environment has no variables to receive, so the function fn2 execution context is removed from the stack. At this point, only the global execution context is left in the call stack, and the code execution of the global execution context is completed.

  1. Stack overflow Call stack size exists, when the stack space is full, there will be stack overflow, as shown in the following code
function stackOverflow(n) {
  if (n <= 0) {
    return 1;
  }
  return n * stackOverflow(n - 1);
}
stackOverflow(50000);

Uncaught RangeError: Maximum Call Stack Size exceeded
Copy the code

The above function is used to calculate factorial. When the recursive call is made for 50,000 times, that is, each time the function is executed, an execution context will be generated and stored in the call stack. The size of the call stack is 12540 when THE chrome81 version browser I use is used, so the stack overflow will occur when the size exceeds the call stack. So how to solve it? There are two ways to do this:

  1. Store the result of each execution into the macro task queue with setTimeout;
  2. Through the way of tail recursion, but many browsers are not compatible, tail recursion is meaningless, it is better to direct loop;

Block-level scope

Due to JavaScript variable enhancement, there is a lot of unintuitive code,

var myName = "Jackson";

if (true) {
  var myName = "Monchic";
}
console.log(myName);

// Output: Monchic
Copy the code

Normal logic should output Jackson, but due to the relationship of variable promotion, the output is Monchic, which has to be said to be a design defect of JavaScript, es6 proposed block-level scope, solved the dependent variable promotion caused a lot of intuitively inconsistent code.

Block-level scope is a block of code wrapped in braces, such as functions, judgment statements, looping statements, individual blocks, etc., as follows:

/ / if block
if (1) {}/ / while block
while (1) {}

/ / function block
function foo() {}

/ / a for loop
for (let i = 0; i < 100; i++) {}

// A single block{}Copy the code

Learn about block-level scope and rewrite the code with let

let myName = "Jackson";

if (true) {
  let myName = "Monchic";
}
console.log(myName);

// Output: Jackson
Copy the code

The result is what we expect. We know that variables declared by var are stored in the execution context. Where are variables declared by let and const?

How do JavaScript engines support block-level scope?

We can declare block-level scope with let and const. We can use let, const, and var in a piece of code. JavaScript engines can support variable promotion and block-level scope at the same time. Let’s start with some code:

function fn() {
  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);
}
fn();
Copy the code

Combined with the idea of execution context mentioned above, the idea of execution context is used to analyze the storage situation in memory. The block-level scope is stored in the lexical environment of execution context, which is a small stack structure.

  1. Compile and create the execution context, as shown

Analyze the figure above:

  • Variables A and C declared by var are stored in the variable environment within the execution context
  • The variable B declared by let is stored in the lexical environment within the execution context
  1. Compiling block-level scoped code When executing block-level scoped code, a in the variable environment is set to 1 and b in the lexical environment is set to 2, while compiling the block-level scoped code, as shown in the figure

Analyze the figure above:

  • Inside the block level, variable B is declared through let. Variable B in this area does not affect external variable B. It is pushed in lexical environment and pushed out after execution is completed.
  • Variable C is declared internally by var at block level. It will be promoted during compilation and stored in the variable environment c = undefined. Variable C will be set to 4 when executing the code.
  • Variable D is declared internally by let at block level. When variable D is compiled, the compiled variable is pushed in the blessed environment, but when executed, variable C will be set to 5.

In a lexical environment, a small stack structure is maintained. The bottom of the stack is the outermost variable declared by let or const. When it enters a scope, variables declared by let or const within that scope are stored at the top of the stack. When console.log(a) is executed, the JavaScript engine looks for variables in the lexical environment first, returns them to the JavaScript engine if found, and continues the search in the variable environment if not found. As shown in the figure below

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.

Conclusion:

  1. In JavaScript code, variable promotion needs to be done first, because the JavaScript code needs to be done before it is executedcompile;
  2. At compile time, variables and functions are stored inThe variable environment, the default value of the variable is undefined; Block-level scopes are stored inLexical environment;
  3. At compile time, there are two identical functions, and the later one overrides the previous one;
  4. When a function is called, the JavaScript engine maintains the data structure of a stack (the call stack). Each call pushes the execution context of the function onto the call stack, and the JavaScript engine starts executing the function code.
  5. When a function completes execution, the JavaScript engine pops the function’s execution context off the stack.
  6. Throws when the allocated stack control is fullStack overflowThe situation;

In the last example, when we look for a variable, we first look in the lexical environment. If we can’t find it, we look in the variable environment until we find it. What is the underlying logic of this search? How is this defined? What causes closures? And so on, the JavaScript engine works, so watch the next section “JavaScript Mechanics 2.”