譯者序
最近在研究 JavaScript 基礎性的東西,可是看到對於執行上下文的解釋我發現有兩種,一種是執行上下文包含:scope
(做用域)、variable object
(變量對象)、this value
(this 值),另一個種是包含:lexical environment
(詞法環境)、variable environment
(變量環境)、this value
(this 值)。javascript
後面我查閱了很多博客以及 ES3 和 ES5 的規範才瞭解到,第一種是 ES3 的規範,經典書籍《JavaScript 高級程序設計》第三版就是這樣解釋的,也是網上廣爲流傳的一種,另外一種是 ES5 的規範。前端
而後我接着又去翻了 ES2018 中的,發現又有變化了,已經增長了更多的內容了,考慮到這部份內容頗爲複雜,準備後面再進行總結分享,查資料的時候看到這篇講執行上下文(ES5 )的還不錯,因此就翻譯出來先分享給你們。java
之後看到變量對象、活動對象知道是 ES3 裏面的內容,而若是是詞法環境、變量環境這種詞就是 ES5 之後的內容。web
如下是正文:面試
什麼是執行上下文?
簡而言之,執行上下文是計算和執行 JavaScript 代碼的環境的抽象概念。每當 Javascript 代碼在運行的時候,它都是在執行上下文中運行。編程
執行上下文的類型
JavaScript 中有三種執行上下文類型。瀏覽器
-
全局執行上下文 — 這是默認或者說基礎的上下文,任何不在函數內部的代碼都在全局上下文中。它會執行兩件事:建立一個全局的 window 對象(瀏覽器的狀況下),而且設置 this
的值等於這個全局對象。一個程序中只會有一個全局執行上下文。 -
函數執行上下文 — 每當一個函數被調用時, 都會爲該函數建立一個新的上下文。每一個函數都有它本身的執行上下文,不過是在函數被調用時建立的。函數上下文能夠有任意多個。每當一個新的執行上下文被建立,它會按定義的順序(將在後文討論)執行一系列步驟。 -
Eval 函數執行上下文 — 執行在 eval
函數內部的代碼也會有它屬於本身的執行上下文,但因爲 JavaScript 開發者並不常用eval
,因此在這裏我不會討論它。
執行棧
執行棧,也就是在其它編程語言中所說的「調用棧」,是一種擁有 LIFO(後進先出)的數據結構,被用來存儲代碼運行時建立的全部執行上下文。微信
當 JavaScript 引擎第一次遇到你的腳本時,它會建立一個全局的執行上下文而且壓入當前執行棧。每當引擎遇到一個函數調用,它會爲該函數建立一個新的執行上下文並壓入棧的頂部。數據結構
引擎會執行處於棧頂的執行上下文的函數。當該函數執行結束時,執行上下文從棧中彈出,控制流程到達當前棧中的下一個上下文。閉包
讓咱們經過下面的代碼示例來理解:
let a = 'Hello World!';
functionfirst() {
console.log('Inside first function');
second();
console.log('Again inside first function');
}
functionsecond() {
console.log('Inside second function');
}
first();
console.log('Inside Global Execution Context');
上述代碼的執行上下文棧。
當上述代碼在瀏覽器加載時,JavaScript 引擎建立了一個全局執行上下文並把它壓入當前執行棧。當遇到 first()
函數調用時,JavaScript 引擎爲該函數建立一個新的執行上下文並把它壓入當前執行棧的頂部。
當從 first()
函數內部調用 second()
函數時,JavaScript 引擎爲 second()
函數建立了一個新的執行上下文並把它壓入當前執行棧的頂部。當 second()
函數執行完畢,它的執行上下文會從當前棧彈出,而且控制流程到達下一個執行上下文,即 first()
函數的執行上下文。
當 first()
執行完畢,它的執行上下文從棧彈出,控制流程到達全局執行上下文。一旦全部代碼執行完畢,JavaScript 引擎從當前棧中移除全局執行上下文。
怎麼建立執行上下文?
到如今,咱們已經看過 JavaScript 怎樣管理執行上下文了,如今讓咱們瞭解 JavaScript 引擎是怎樣建立執行上下文的。
建立執行上下文有兩個階段:1) 建立階段 和 2) 執行階段。
建立階段
在 JavaScript 代碼執行前,執行上下文將經歷建立階段。在建立階段會發生三件事:
-
this 值的決定,即咱們所熟知的 this 綁定。 -
建立 詞法環境組件。 -
建立 變量環境組件。
因此執行上下文在概念上表示以下:
ExecutionContext = {
ThisBinding = <this value>,
LexicalEnvironment = { ... },
VariableEnvironment = { ... },
}
this 綁定:**
在全局執行上下文中,this
的值指向全局對象。(在瀏覽器中,this
引用 Window 對象)。
在函數執行上下文中,this
的值取決於該函數是如何被調用的。若是它被一個引用對象調用,那麼 this
會被設置成那個對象,不然 this
的值被設置爲全局對象或者 undefined
(在嚴格模式下)。例如:
let foo = {
baz: function() {
console.log(this);
}
}
foo.baz(); // 'this' 引用 'foo', 由於 'baz' 被
// 對象 'foo' 調用
let bar = foo.baz;
bar(); // 'this' 指向全局 window 對象,由於
// 沒有指定引用對象
詞法環境
官方的 ES6[1] 文檔把詞法環境定義爲
詞法環境是一種規範類型,基於 ECMAScript 代碼的詞法嵌套結構來定義標識符和具體變量和函數的關聯。一個詞法環境由環境記錄器和一個可能的引用outer詞法環境的空值組成。
簡單來講詞法環境是一種持有標識符—變量映射的結構。(這裏的標識符指的是變量/函數的名字,而變量是對實際對象[包含函數類型對象]或原始數據的引用)。
如今,在詞法環境的內部有兩個組件:(1) 環境記錄器和 (2) 一個外部環境的引用。
-
環境記錄器是存儲變量和函數聲明的實際位置。 -
外部環境的引用意味着它能夠訪問其父級詞法環境(做用域)。
譯者注:外部環境已經跟 ES3 規定的做用域的做用相似
詞法環境有兩種類型:
-
全局環境(在全局執行上下文中)是沒有外部環境引用的詞法環境。全局環境的外部環境引用是 null。它擁有內建的 Object/Array/等、在環境記錄器內的原型函數(關聯全局對象,好比 window 對象)還有任何用戶定義的全局變量,而且 this
的值指向全局對象。 -
在 函數環境中,函數內部用戶定義的變量存儲在 環境記錄器中。而且引用的外部環境多是全局環境,或者任何包含此內部函數的外部函數。
環境記錄器也有兩種類型(如上!):
-
聲明式環境記錄器存儲變量、函數和參數。 -
對象環境記錄器用來定義出如今 全局上下文中的變量和函數的關係。
簡而言之,
-
在 全局環境中,環境記錄器是對象環境記錄器。 -
在 函數環境中,環境記錄器是聲明式環境記錄器。
注意 — 對於函數環境,聲明式環境記錄器還包含了一個傳遞給函數的 arguments
對象(此對象存儲索引和參數的映射)和傳遞給函數的參數的 length。
抽象地講,詞法環境在僞代碼中看起來像這樣:
GlobalExectionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Object",
// 在這裏綁定標識符
}
outer: <null>
}
}
FunctionExectionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
// 在這裏綁定標識符
}
outer: <Global or outer function environment reference>
}
}
變量環境:
它一樣是一個詞法環境,其環境記錄器持有變量聲明語句在執行上下文中建立的綁定關係。
如上所述,變量環境也是一個詞法環境,因此它有着上面定義的詞法環境的全部屬性。
在 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",
// 在這裏綁定標識符
a: < uninitialized >,
b: < uninitialized >,
multiply: < func >
}
outer: <null>
},
VariableEnvironment: {
EnvironmentRecord: {
Type: "Object",
// 在這裏綁定標識符
c: undefined,
}
outer: <null>
}
}
FunctionExectionContext = {
ThisBinding: <Global Object>,
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
// 在這裏綁定標識符
Arguments: {0: 20, 1: 30, length: 2},
},
outer: <GlobalLexicalEnvironment>
},
VariableEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
// 在這裏綁定標識符
g: undefined
},
outer: <GlobalLexicalEnvironment>
}
}
注意 — 只有遇到調用函數 multiply
時,函數執行上下文才會被建立。
可能你已經注意到 let
和 const
定義的變量並無關聯任何值,但 var
定義的變量被設成了 undefined
。
這是由於在建立階段時,引擎檢查代碼找出變量和函數聲明,雖然函數聲明徹底存儲在環境中,可是變量最初設置爲 undefined
(var
狀況下),或者未初始化(let
和 const
狀況下)。
這就是爲何你能夠在聲明以前訪問 var
定義的變量(雖然是 undefined
),可是在聲明以前訪問 let
和 const
的變量會獲得一個引用錯誤。
這就是咱們說的變量聲明提高。
執行階段
這是整篇文章中最簡單的部分。在此階段,完成對全部這些變量的分配,最後執行代碼。
注意 — 在執行階段,若是 JavaScript 引擎不能在源碼中聲明的實際位置找到 let
變量的值,它會被賦值爲 undefined
。
結論
咱們已經討論過 JavaScript 程序內部是如何執行的。雖然要成爲一名卓越的 JavaScript 開發者並不須要學會所有這些概念,可是若是對上面概念能有不錯的理解將有助於你更輕鬆,更深刻地理解其餘概念,如變量聲明提高,做用域和閉包。
原文連接是:https://blog.bitsrc.io/understanding-execution-context-and-execution-stack-in-javascript-1c9ea8642dd0
參考資料
官方的 ES6: http://ecma-international.org/ecma-262/6.0/
交流討論
歡迎關注公衆號「前端試煉」,公衆號平時會分享一些實用或者有意思的東西,發現代碼之美。專一深度和最佳實踐,但願打造一個高質量的公衆號。
公衆號後臺回覆「加羣」,拉你進交流面試羣。
若是你不想加羣,只是想加我也是能夠的。
若是以爲這篇文章還不錯,來個【分享、點贊、在看】三連吧,讓更多的人也看到~
本文分享自微信公衆號 - 前端試煉(code-photo)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。