瞭解Javascript中的執行上下文和執行堆疊

Eric_暱稱已被使用發表於2018-11-03
By Sukhjinder Arora | Aug 28, 2018

原文

如果你是或者你想要成為一名js開發者,那麼你必須瞭解js程式內部的運作。理解執行上下文和執行堆疊對於理解js的其它概念(如提升,範圍和閉包)至關重要。

正確地理解執行上下文和執行堆疊將幫助你更好地使用js開發應用。

廢話少說,讓我們開始吧:)

* * *

什麼是執行上下文?

簡單來說,執行上下文是預估和執行當前環境下js程式碼的抽象概念。每當在js中執行程式碼時,它都在執行上下文中執行。

(譯者:emmm,就是執行上下文包含了追蹤當前正在執行的程式碼的全部狀態。)

執行上下文的型別

在js中有三種執行型別

  • 全域性執行上下文——這是預設或者說基礎執行上下文。函式外的程式碼就處於全域性執行上下文中。它做了兩件事:它建立了window物件(在瀏覽器環境下),也就是全域性物件。並把this指向全域性物件。在程式裡面只能有一個全域性上下文。

  • 函式執行上下文——每次函式被呼叫,都會為這個函式建立一個新的上下文。每個函式都有自己的上下文,但是隻有被呼叫的時候才會被建立。可以有很多個函式執行上下文。每當建立一個新的函式執行上下文,js引擎都會按照定義好的順序執行一系列的步驟,我將會在下文中討論。

  • Eval 函式的執行上下文——eval函式執行的時候也會為它裡面的程式碼建立上下文,但是這個方法用的少,在本文略過。

執行堆疊

執行堆疊在其它語言中被稱為“呼叫棧”,是一種先進後出的一種資料結構,在程式碼執行期間被用於儲存所有的執行上下文。

當js引擎開始解析js程式碼時,會先建立全域性執行上下文並且放在當前執行堆疊中。每當引擎遇到函式呼叫的程式碼時,都會建立該函式的上下文並推入當前執行堆疊中。

引擎執行位於執行堆疊頂部的方法。當方法執行完畢,執行堆疊pop掉最頂部的上下文,接著引擎繼續執行堆疊頂部的方法。

用程式碼示範一下:

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');
複製程式碼
瞭解Javascript中的執行上下文和執行堆疊

上述程式碼的執行上下文堆疊。

當上述程式碼在瀏覽器內載入,js引擎就會建立一個全域性執行上下文並且把它推入當前執行堆疊中。當呼叫first()時,js為該函式建立一個新的執行上下文,並且把它推入到當前執行堆疊。

當second()方法被first()呼叫,js引擎為該方法建立一個新的執行上下文並把它推入當前執行堆疊。當second()執行完畢,這個方法的上下文就被執行堆疊推出,並且執行下一個執行上下文,也就是first()。

當first()執行完畢,重複以上步驟。一旦執行了所有程式碼,JavaScript引擎就會從當前堆疊中刪除全域性執行上下文。

執行上下文如何建立?

直到現在,我們已經知道js引擎如何管理執行上下文的了,現在讓我們瞭解下執行上下文如何被js建立的。

建立執行上下文有兩個階段:1)建立階段,2)執行階段(譯者:???懵逼臉)。

建立階段

在執行任何JavaScript程式碼之前,執行上下文將經歷建立階段。在建立階段會發生三件事:

  1. this繫結
  2. 詞法環境(LexicalEnvironment)建立
  3. 變數環境(VariableEnvironment)建立

(譯者:VariableEnvironment和LexicalEnvironment譯者也是第一次聽到,慚愧,大學沒學過編譯原理,在js中還有個this繫結,似乎是js特有)

因此,執行上下文在概念上可以這樣表示,如下:

ExecutionContext = { 
ThisBinding = <
this value>
, LexicalEnvironment = {
...
}, VariableEnvironment = {
...
},
}複製程式碼

this繫結

在全域性執行上下文中,this值指向全域性物件(在瀏覽器內是window物件)。

在函式執行上下文中,this的值取決於函式的呼叫的時候的情況。如果它由物件引用呼叫,this值就是該物件,否則this值指向全域性或者為undefined(在嚴格模式下)。

例如:

let person = { 
name: 'peter', birthYear: 1994, calcAge: function() {
console.log(2018 - this.birthYear);

}
}person.calcAge();
// 'this' refers to 'person', because 'calcAge' was called with //'person' object referencelet calculateAge = person.calcAge;
calculateAge();
// 'this' refers to the global window object, because no object reference was given複製程式碼

詞法環境

官方es6文件對詞法環境有如下解釋:

詞彙環境是一種規範型別,用於根據ECMAScript程式碼的詞法巢狀結構定義識別符號與特定變數和函式的關聯。詞彙環境由environment record(譯者:實在不知道咋翻)和外部詞彙環境的可能為null的引用組成。

(譯者:硬翻的,有點怪)

簡而言之,詞彙環境是一種包含識別符號變數對映的結構(此處識別符號指的是變數/函式的名稱,變數是對實際物件【包括函式型別的物件】或原始值的引用)。

現在,詞法環境由兩部分組成:

(1)environment record

(2)外部環境的引用

1、environment record是存放變數和函式宣告的一個地方

2、對外部環境的引用意味著它可以訪問其外部詞彙環境。

有兩種型別的詞法環境:

  • 一種是全域性環境(在全域性執行上下文裡),它沒有外部環境的引用。它的外部環境引用為null。它有全域性物件(window物件)和關聯的方法和屬性(例如陣列方法),以及任何使用者定義的全域性變數,並且this的值指向全域性物件。

  • 一種是函式環境,它存放使用者在函式裡定義的變數。並且外部環境可以指向全域性環境,或者是外層函式環境。

筆記——對於函式環境,environment record還包含一個arguments物件,該物件包含索引和傳遞給函式的引數之間的對映以及傳遞給函式的引數的長度(數量)。例如,下面函式的引數物件如下所示:

function foo(a, b) { 
var c = a + b;

}foo(2, 3);
// argument objectArguments: {0: 2, 1: 3, length: 2
},複製程式碼

environment record也有兩種型別:

  • 宣告性environment record儲存變數,函式和引數。 一個函式環境包含宣告性environment record。

  • 物件environment record用於定義在全域性執行上下文中出現的變數和函式的關聯。全域性環境包含物件environment record。

抽象地說,詞法環境在虛擬碼中看起來像這樣:

GlobalExectionContext = { 
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Object", // Identifier bindings go here
} outer: <
null>

}
}FunctionExectionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Declarative", // Identifier bindings go here
} outer: <
Global or outer function environment reference>

}
}複製程式碼

變數環境

它也是一個詞法環境,其EnvironmentRecord包含由此執行上下文中的VariableStatements建立的繫結。

如上所述,變數環境也是一個詞彙環境,因此它具有上面定義的詞法環境的所有屬性。

在es6,詞法環境和變數環境的不同在於前者用於儲存函式宣告和變數(let和const)繫結,而後者用於儲存變數(var)的繫結。

來看個例子:

let a = 20;
const b = 30;
var c;
function multiply(e, f) {
var g = 20;
return e * f * g;

}c = multiply(20, 30);
複製程式碼

然後執行上下文會像這樣:

GlobalExectionContext = { 
ThisBinding: <
Global Object>
, LexicalEnvironment: {
EnvironmentRecord: {
Type: "Object", // Identifier bindings go here a: <
uninitialized >
, b: <
uninitialized >
, multiply: <
func >

} outer: <
null>

}, VariableEnvironment: {
EnvironmentRecord: {
Type: "Object", // Identifier bindings go here c: undefined,
} outer: <
null>

}
}FunctionExectionContext = {
ThisBinding: <
Global Object>
, LexicalEnvironment: {
EnvironmentRecord: {
Type: "Declarative", // Identifier bindings go here Arguments: {0: 20, 1: 30, length: 2
},
}, outer: <
GlobalLexicalEnvironment>

},VariableEnvironment: {
EnvironmentRecord: {
Type: "Declarative", // Identifier bindings go here g: undefined
}, outer: <
GlobalLexicalEnvironment>

}
}複製程式碼

筆記——函式multiply呼叫時才會建立函式執行上下文。

正如你所注意到的一樣,let和const定義的變數沒有繫結任何值,但var定義的變數為undefined

這是因為在建立階段,掃描程式碼尋找變數和函式宣告時,函式宣告完全儲存在環境中,但變數最初設定為undefined(var)或保持為為初始化(let、const)。

(譯者:就是var會宣告提升,而let和const不會)

這就是為什麼你可以在變數宣告前訪問到var定義的變數,而訪問let和const定義的變數則會丟擲引用錯誤。

這就是js的變數提升。

執行階段

這是整篇文章中最簡單的部分。 在此階段,完成對所有這些變數的分配,最後執行程式碼。

筆記——在執行階段,如果js引擎在原始碼宣告的實際位置找不到let變數的值,那麼它將為其分配undefined值。

結論

現在,我們已經了js的部分執行原理,雖然理解了這些概念不一定能讓你成為出色的js開發者,但是明白了上述的概念能讓你更好理解js的其它概念,例如變數提升、閉包。

來源:https://juejin.im/post/5bdd48b551882516ee0882f8

相關文章