理解 Javascript 執行上下文和執行棧
如果你是一名 JavaScript 開發者,或者想要成為一名 JavaScript 開發者,那麽你必須知道 JavaScript 程序內部的執行機制。理解執行上下文和執行棧同樣有助於理解其他的 JavaScript 概念如提升機制、作用域和閉包等。
正確理解執行上下文和執行棧的概念將有助於你成為一名更好的 JavaScript 開發人員。
廢話不多說,讓我們切入正題。
什麽是執行上下文
簡而言之,執行上下文就是當前 JavaScript 代碼被解析和執行時所在環境的抽象概念, JavaScript 中運行任何的代碼都是在執行上下文中運行。
執行上下文的類型
執行上下文總共有三種類型:
-
全局執行上下文:
this
?指針指向這個全局對象。一個程序中只能存在一個全局執行上下文。 - 函數執行上下文:?每次調用函數時,都會為該函數創建一個新的執行上下文。每個函數都擁有自己的執行上下文,但是只有在函數被調用的時候才會被創建。一個程序中可以存在任意數量的函數執行上下文。每當一個新的執行上下文被創建,它都會按照特定的順序執行一系列步驟,具體過程將在本文後面討論。
-
Eval 函數執行上下文:?運行在?
eval
?函數中的代碼也獲得了自己的執行上下文,但由於 Javascript 開發人員不常用 eval 函數,所以在這裏不再討論。
執行棧
執行棧,在其他編程語言中也被叫做調用棧,具有 LIFO(後進先出)結構,用於存儲在代碼執行期間創建的所有執行上下文。
當 JavaScript 引擎首次讀取你的腳本時,它會創建一個全局執行上下文並將其推入當前的執行棧。每當發生一個函數調用,引擎都會為該函數創建一個新的執行上下文並將其推到當前執行棧的頂端。
引擎會運行執行上下文在執行棧頂端的函數,當此函數運行完成後,其對應的執行上下文將會從執行棧中彈出,上下文控制權將移到當前執行棧的下一個執行上下文。
讓我們通過下面的代碼示例來理解這一點:
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 引擎會創建一個全局執行上下文並且將它推入當前的執行棧。當調用?first()
?函數時,JavaScript 引擎為該函數創建了一個新的執行上下文並將其推到當前執行棧的頂端。
當在?first()
?函數中調用?second()
?函數時,Javascript 引擎為該函數創建了一個新的執行上下文並將其推到當前執行棧的頂端。當?second()
?函數執行完成後,它的執行上下文從當前執行棧中彈出,上下文控制權將移到當前執行棧的下一個執行上下文,即?first()
?函數的執行上下文。
當?first()
?函數執行完成後,它的執行上下文從當前執行棧中彈出,上下文控制權將移到全局執行上下文。一旦所有代碼執行完畢,Javascript 引擎把全局執行上下文從執行棧中移除。
執行上下文是如何被創建的
到目前為止,我們已經看到了 JavaScript 引擎如何管理執行上下文,現在就讓我們來理解 JavaScript 引擎是如何創建執行上下文的。
執行上下文分兩個階段創建:?1)創建階段;?2)執行階段
創建階段
在任意的 JavaScript 代碼被執行前,執行上下文處於創建階段。在創建階段中總共發生了三件事情:
- 確定?this?的值,也被稱為?This Binding?。
- LexicalEnvironment(詞法環境)?組件被創建。
- VariableEnvironment(變量環境)?組件被創建。
因此,執行上下文可以在概念上表示如下:
ExecutionContext = {
ThisBinding = <this value>,
LexicalEnvironment = { ... },
VariableEnvironment = { ... },
}
This Binding:
在全局執行上下文中,?this
?的值指向全局對象,在瀏覽器中,?this
?的值指向 window 對象。
在函數執行上下文中,?this
?的值取決於函數的調用方式。如果它被一個對象引用調用,那麽?this
?的值被設置為該對象,否則?this
?的值被設置為全局對象或?undefined
?(嚴格模式下)。例如:
let person = {
name: ‘peter‘,
birthYear: 1994,
calcAge: function() {
console.log(2018 - this.birthYear);
}
}
person.calcAge();
// ‘this‘ 指向 ‘person‘, 因為 ‘calcAge‘ 是被 ‘person‘ 對象引用調用的。
let calculateAge = person.calcAge;
calculateAge();
// ‘this‘ 指向全局 window 對象,因為沒有給出任何對象引用
詞法環境(Lexical Environment)
官方 ES6 文檔將詞法環境定義為:
詞法環境是一種規範類型,基於 ECMAScript 代碼的詞法嵌套結構來定義標識符與特定變量和函數的關聯關系。詞法環境由環境記錄(environment record)和可能為空引用(null)的外部詞法環境組成。
簡而言之,詞法環境是一個包含?標識符變量映射?的結構。(這裏的?標識符?表示變量/函數的名稱,?變量?是對實際對象【包括函數類型對象】或原始值的引用)
在詞法環境中,有兩個組成部分:(1)?環境記錄(environment record)?(2)?對外部環境的引用
- 環境記錄?是存儲變量和函數聲明的實際位置。
- 對外部環境的引用?意味著它可以訪問其外部詞法環境。
詞法環境有兩種類型:
- 全局環境(在全局執行上下文中)是一個沒有外部環境的詞法環境。全局環境的外部環境引用為?null?。它擁有一個全局對象(window 對象)及其關聯的方法和屬性(例如數組方法)以及任何用戶自定義的全局變量,?
this
?的值指向這個全局對象。 - 函數環境,用戶在函數中定義的變量被存儲在?環境記錄?中。對外部環境的引用可以是全局環境,也可以是包含內部函數的外部函數環境。
註意:對於?函數環境?而言,?環境記錄?還包含了一個?arguments
?對象,該對象包含了索引和傳遞給函數的參數之間的映射以及傳遞給函數的參數的?長度(數量)?。例如,下面函數的?arguments
?對象如下所示:
function foo(a, b) {
var c = a + b;
}
foo(2, 3);
// arguments 對象
Arguments: {0: 2, 1: 3, length: 2},
環境記錄同樣有兩種類型(如下所示):
- 聲明性環境記錄?存儲變量、函數和參數。一個函數環境包含聲明性環境記錄。
- 對象環境記錄?用於定義在全局執行上下文中出現的變量和函數的關聯。全局環境包含對象環境記錄。
抽象地說,詞法環境在偽代碼中看起來像這樣:
GlobalExectionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Object",
// 標識符綁定在這裏
outer: <null>
}
}
FunctionExectionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
// 標識符綁定在這裏
outer: <Global or outer function environment reference>
}
}
變量環境:
它也是一個詞法環境,其?EnvironmentRecord
?包含了由?VariableStatements?在此執行上下文創建的綁定。
如上所述,變量環境也是一個詞法環境,因此它具有上面定義的詞法環境的所有屬性。
在 ES6 中,?LexicalEnvironment?組件和?VariableEnvironment?組件的區別在於前者用於存儲函數聲明和變量(?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://www.jianshu.com/p/436...
原文地址:https://segmentfault.com/a/1190000016912433
理解 Javascript 執行上下文和執行棧