忽然以爲對於一名JavaScript開發者而言,須要知道JavaScript程序內部是如何運行的,那麼對於此章節執行上下文和執行棧的理解很重要,對理解其餘JavaScript概念(變量聲明提示,做用域和閉包)都有幫助。javascript
看了不少相關文章,寫得很好,總結了ES3以及ES6對於執行上下文概念的描述,以及新的概念介紹。java
簡而言之,執行上下文是評估和執行 JavaScript 代碼的環境的抽象概念。每當 Javascript 代碼在運行的時候,它都是在執行上下文中運行。node
JavaScript 中有三種執行上下文類型express
this
的值等於這個全局對象。一個程序中只會有一個全局執行上下文。eval
函數內部的代碼也會有它屬於本身的執行上下文,但因爲 JavaScript 開發者並不常用 eval
,因此在這裏我不會討論它。執行上下文是一個抽象的概念,咱們能夠將它理解爲一個 object
,一個執行上下文裏包括如下內容:編程
variable object
簡稱 VO
)每一個執行環境文都有一個表示變量的對象——變量對象,全局執行環境的變量對象始終存在,而函數這樣局部環境的變量,只會在函數執行的過程當中存在,在函數被調用時且在具體的函數代碼運行以前,JS 引擎會用當前函數的參數列表(arguments
)初始化一個 「變量對象」 並將當前執行上下文與之關聯 ,函數代碼塊中聲明的 變量 和 函數 將做爲屬性添加到這個變量對象上。數組
有一點須要注意,只有函數聲明(function declaration)會被加入到變量對象中,而函數表達式(function expression)會被忽略。
複製代碼
// 這種叫作函數聲明,會被加入變量對象
function demo () {}
// tmp 是變量聲明,也會被加入變量對象,可是做爲一個函數表達式 demo2 不會被加入變量對象
var tmp = function demo2 () {}
複製代碼
全局執行上下文和函數執行上下文中的變量對象還略有不一樣,它們之間的差異簡單來講:瀏覽器
window
對象。VO
)被激活爲活動對象(AO
)時,咱們才能訪問到其中的屬性和方法。activation object
簡稱 AO
)函數進入執行階段時,本來不能訪問的變量對象被激活成爲一個活動對象,自此,咱們能夠訪問到其中的各類屬性。bash
其實變量對象和活動對象是一個東西,只不過處於不一樣的狀態和階段而已。數據結構
scope chain
)做用域 規定了如何查找變量,也就是肯定當前執行代碼對變量的訪問權限。當查找變量的時候,會先從當前上下文的變量對象中查找,若是沒有找到,就會從父級(詞法層面上的父級)執行上下文的變量對象中查找,一直找到全局上下文的變量對象,也就是全局對象。這樣由多個執行上下文的變量對象構成的鏈表就叫作 做用域鏈。閉包
若是當前函數被做爲對象方法調用或使用 bind
call
apply
等 API
進行委託調用,則將當前代碼塊的調用者信息(this value
)存入當前執行上下文,不然默認爲全局對象調用。
關於 this
的建立細節,有點煩,有興趣的話能夠進入 這個章節 學習。
若是將上述一個完整的執行上下文使用代碼形式表現出來的話,應該相似於下面這種:
executionContext:{
[variable object | activation object]:{
arguments,
variables: [...],
funcions: [...]
},
scope chain: variable object + all parents scopes
thisValue: context object
}
複製代碼
執行上下文的生命週期有三個階段,分別是:
函數執行上下文的建立階段,發生在函數調用時且在執行函數體內的具體代碼以前,在建立階段,JS 引擎會作以下操做:
執行全局代碼前,建立一個全局執行上下文
對全局數據進行預處理
有沒有發現這個建立執行上下文的階段有變量和函數的初始化生命。這個操做就是 **變量聲明提高**(變量和函數聲明都會提高,可是函數提高更靠前)。
複製代碼
執行階段中,JS 代碼開始逐條執行,在這個階段,JS 引擎開始對定義的變量賦值、開始順着做用域鏈訪問變量、若是內部有函數調用就建立一個新的執行上下文壓入執行棧並把控制權交出……
通常來說當函數執行完成後,當前執行上下文(局部環境)會被彈出執行上下文棧而且銷燬,控制權被從新交給執行棧上一層的執行上下文。
注意這只是通常狀況,閉包的狀況又有所不一樣。
閉包的定義:有權訪問另外一個函數內部變量的函數。簡單說來,若是一個函數被做爲另外一個函數的返回值,並在外部被引用,那麼這個函數就被稱爲閉包。
對於 ES3
中的執行上下文,咱們能夠用下面這個列表來歸納程序執行的整個過程:
arguments object
檢查上下文中的參數,初始化名稱和值並建立引用副本undefined
來初始化this
值ES5
規範又對 ES3
中執行上下文的部分概念作了調整,最主要的調整,就是去除了 ES3
中變量對象和活動對象,以 詞法環境組件( LexicalEnvironment component) 和 變量環境組件( VariableEnvironment component) 替代。因此 ES5
的執行上下文概念上表示大概以下:
ExecutionContext = {
ThisBinding = <this value>, LexicalEnvironment = { ... }, VariableEnvironment = { ... }, } 複製代碼
this
的值指向全局對象,在瀏覽器中this
的值指向 window
對象,而在nodejs
中指向這個文件的module
對象。this
的值取決於函數的調用方式。具體有:默認綁定、隱式綁定、顯式綁定(硬綁定)、new
綁定、箭頭函數,具體內容會在【this全面解析】部分詳解。詞法環境有兩個組成部分
詞法環境有兩種類型
this
的值指向這個全局對象。arguments
對象。對外部環境的引用能夠是全局環境,也能夠是包含內部函數的外部函數環境。直接看僞代碼可能更加直觀
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>
}
}
複製代碼
變量提高的緣由:在建立階段,函數聲明存儲在環境中,而變量會被設置爲 undefined
(在 var
的狀況下)或保持未初始化(在 let
和 const
的狀況下)。因此這就是爲何能夠在聲明以前訪問 var
定義的變量(儘管是 undefined
),但若是在聲明以前訪問 let
和 const
定義的變量就會提示引用錯誤的緣由。這就是所謂的變量提高。
對於 ES5
中的執行上下文,咱們能夠用下面這個列表來歸納程序執行的整個過程:
let
和 const
定義的變量)var
定義的變量,初始值爲 undefined
形成聲明提高)this
值爲全局對象(以瀏覽器爲例,就是 window
)let
和 const
定義的變量)var
定義的變量,初始值爲 undefined
形成聲明提高)this
值執行棧,也就是在其它編程語言中所說的「調用棧」,是一種擁有 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()
函數建立了一個新的執行上下文並把它壓入當前執行棧的頂部。當 second()
函數執行完畢,它的執行上下文會從當前棧彈出,而且控制流程到達下一個執行上下文,即 first()
函數的執行上下文。
當 first()
執行完畢,它的執行上下文從棧彈出,控制流程到達全局執行上下文。一旦全部代碼執行完畢,JavaScript 引擎從當前棧中移除全局執行上下文。