Deep understanding of JavaScript execution context and execution stack
Foreword
If you are a JavaScript developer, or want to be a JavaScript developer, then you must know the internal execution mechanism of JavaScript programs. Execution context and execution stack are one of the key concepts in JavaScript and one of the difficulties in JavaScript. Understanding execution context and execution stack also helps to understand other JavaScript concepts such as hoisting, scope, and closures. This article tries to introduce these concepts in a way that is as easy to understand as possible.
1. Execution Context
1. What is an execution context
In short, the execution context is the abstract concept of the environment in which the current JavaScript code is parsed and executed. Any code running in JavaScript is run in the execution context.
2. The type of execution context
There are three types of execution contexts in total:
- Global execution context: This is the default and most basic execution context. Code that is not in any function is in the global execution context. It does two things: 1. Creates a global object, which in the browser is the window object. 2. Point the this pointer to the global object. Only one global execution context can exist in a program.
- Function execution context: Every time a function is called, a new execution context is created for that function. Each function has its own execution context, but it is only created when the function is called. Any number of function execution contexts can exist in a program. Whenever a new execution context is created, it executes a series of steps in a specific order, discussed later in this article.
- Eval function execution context: The code running in the eval function also gets its own execution context, but since the eval function is not commonly used by Javascript developers, it will not be discussed here.
Second, the life cycle of the execution context
The life cycle of the execution context includes three stages: creation stage → execution stage → recycling stage , and this article focuses on the creation stage.
1. Creation stage
When a function is called, but before executing any of its internal code, it does three things:
- Create a variable object: first initialize the function's argument arguments, lift the function declaration and variable declaration. It will be explained in detail below.
- Create a scope chain (Scope Chain): During the creation phase of the execution-time context, the scope chain is created after the variable object. The scope chain itself contains variable objects. The scope chain is used to resolve variables. When asked to resolve a variable, JavaScript always starts from the innermost level of code nesting. If the variable is not found at the innermost level, it will jump to the parent scope to look for it until the variable is found.
- Determine this point: including a variety of situations, which will be explained in detail below
Before a JS script is executed, the code must be parsed (so JS is a scripting language for interpretation and execution). When parsing, a global execution context will be created first, and the variables and function declarations to be executed in the code will be taken out first. Variables are temporarily assigned to undefined, and functions are declared ready for use. This step is done, and then the formal execution process begins.
In addition, before a function is executed, it will also create a function execution context, which is similar to the global context, but there will be more this arguments and function parameters in the function execution context.
2. Execution stage
Perform variable assignment, code execution
3. Recycling stage
The execution context is popped from the stack and waiting for the virtual machine to recycle the execution context
3. Variable promotion and details of this pointing
1. Variable declaration promotion
Most programming languages declare variables before using them, but in JS, things are a little different:
console.log(a)// undefined
var a = 10
The above code outputs normally undefinedinstead of reporting an error Uncaught ReferenceError
: a is not definedbecause of hoisting, which is equivalent to the following code:
var a; //declare the default value is undefined "ready to work"
console.log(a);
a=10; //assignment
2. Function declaration promotion
We all know that there are two ways to create a function, one is through function declaration function foo(){}
and the other is through function expression var foo = function(){}
. What is the difference between these two in function promotion?
console.log(f1) // function f1(){}
function f1() {} // function declaration
console.log(f2) // undefined
var f2 = function() {} // function expression
Next, we illustrate this problem with an example:
function test() {
foo(); // Uncaught TypeError "foo is not a function"
bar(); // "this will run!"
var foo = function () { // function expression assigned to local variable 'foo'
alert("this won't run!");
}
function bar() { // function declaration, given the name 'bar'
alert("this will run!");
}
}
test();
In the above example, foo()
is called with an error, but bar can be called normally.
We said earlier that both variables and functions will rise. When a function expression var foo = function(){
} is encountered , it will first var foorise to the top of the function body. However, the value of foo is undefined at this time, so the execution reports an foo()
error.
For functions bar(
), the entire function is promoted so bar()
that it can be executed smoothly.
There is one detail that must be noted: when a function and a variable have the same name and both are promoted, the function declaration has a higher priority, so the variable declaration will be overwritten by the function declaration, but can be reassigned.
alert(a);//Output: function a(){ alert('I am a function') }
function a(){ alert('I am a function') }//
var a = 'I am a variable';
alert(a); //output: 'I am a variable'
The function declaration has higher precedence than the var declaration, which means that when two variables with the same name are declared by function and var at the same time, the function declaration will override the var declaration
This code is equivalent to:
function a(){alert('I am a function')}
var a; //hoisting
alert(a); //Output: function a(){ alert('I am a function') }
a = 'I am a variable';//Assignment
alert(a); //output: 'I am a variable'
Finally, let's look at a more complex example:
function test(arg){
// 1. The formal parameter arg is "hi"
// 2. Because function declarations have higher priority than variable declarations, arg is function at this time
console.log(arg);
var arg = 'hello'; // 3. var arg variable declaration is ignored, arg = 'hello' is executed
function arg(){
console.log('hello world')
}
console.log(arg);
}
test('hi');
/* output:
function arg(){
console.log('hello world')
}
hello
*/
This is because when the function is executed, a new private scope is first formed, and then the following steps are followed:
- If there is a formal parameter, assign a value to the formal parameter first
- Perform pre-interpretation in private scope, function declaration has higher priority than variable declaration, and finally the latter will be overwritten by the former, but can be reassigned
- Code in private scope is executed from top to bottom
3. Determine the point of this
First understand a very important concept - the value of this can only be confirmed when it is executed, and it cannot be confirmed when it is defined! Why - because this is part of the execution context, and the execution context needs to be determined before the code is executed, not when it is defined. See the following example:
// case 1
function foo() {
console.log(this.a) //1
}
var a = 1
foo()
// case 2
function fn(){
console.log(this);
}
var obj={fn:fn};
obj.fn(); //this->obj
// case 3
function CreateJsPerson(name,age){
//this is an instance of the current class p1
this.name=name; //=>p1.name=name
this.age=age; //=>p1.age=age
}
var p1=new CreateJsPerson("Yin Huazhi",48);
// case 4
function add(c, d){
return this.a + this.b + c + d;
}
var o = {a:1, b:3};
add.call(o, 5, 7); // 1 + 3 + 5 + 7 = 16
add.apply(o, [10, 20]); // 1 + 3 + 10 + 20 = 34
// case 5
<button id="btn1">arrow function this</button>
<script type="text/javascript">
let btn1 = document.getElementById('btn1');
let obj = {
name: 'kobe',
age: 39,
getName: function () {
btn1.onclick = () => {
console.log(this);//obj
}
};
}
};
obj.getName();
</script>
Next, we explain the above situations one by one
- For calling foo directly, no matter where the foo function is placed, this must be window
- For
obj.foo()
, we only need to remember that whoever calls the function is this, so in this scenario, this in the foo function is the obj object - In the constructor mode, this in this xxx=xxx that appears in the class (in the function body) is an instance of the current class
- call, apply and bind: this is the first parameter
- The arrow function this points to: the arrow function does not have its own this, to see if there is a function in the outer layer, if there is, the this of the outer function is the this of the inner arrow function, if not, then this is the window.
Fourth, the execution context stack (Execution Context Stack)
There are many functions, and there are multiple function execution contexts. Each time a function is called, a new execution context is created. How to manage so many execution contexts created?
The JavaScript engine creates an execution context stack to manage execution contexts. The execution context stack can be regarded as a stack structure that stores function calls, following the principle of first-in, last-out .
From the above flowchart, we need to remember a few key points:
- JavaScript executes on a single thread, and all code is queued for execution.
- When the browser starts executing global code, it first creates a global execution context and pushes it to the top of the execution stack.
- Whenever execution of a function is entered, the function's execution context is created and pushed onto the top of the execution stack. After the execution of the current function is completed, the execution context of the current function is popped off the stack and waits for garbage collection.
- The browser's JS execution engine always accesses the execution context at the top of the stack.
- There is only one global context, and it is popped from the stack when the browser is closed.
Let's look at another example:
var color = 'blue';
function changeColor() {
var anotherColor = 'red';
function swapColors() {
var tempColor = anotherColor;
anotherColor = color;
color = tempColor;
}
swapColors();
}
changeColor();
The above code runs as follows:
- When the above code is loaded in the browser, the JavaScript engine creates a global execution context and pushes it onto the current execution stack
- When the changeColor function is called, the internal code of the changeColor function has not yet been executed. The js execution engine immediately creates an execution context for changeColor (EC for short), and then pushes the execution context into the execution stack (ECStack for short).
- During the execution of the changeColor function, the swapColors function is called. Similarly, before the swapColors function is executed, an execution context of swapColors is created and pushed into the execution stack.
- The execution of the swapColors function is completed, and the execution context of the swapColors function is popped off the stack and destroyed.
- The execution of the changeColor function is completed, and the execution context of the changeColor function is popped off the stack and destroyed.