若是你想成爲一名優秀的JavaScript 開發者,那你就必須瞭解 JavaScript 程序內部的執行機制。而執行上下文和執行棧是其關鍵概念之一, 理解執行上下文和執行棧一樣有助於理解其餘的 JavaScript 概念如提高機制、做用域和閉包等。javascript
執行上下文和執行棧是JavaScript的難點之一,因此本人儘可能用通俗易懂的方式來闡述這些概念。java
當 JavaScript 代碼執行一段可執行代碼(executable code)時,會建立對應的執行上下文(execution context)。執行上下文(可執行代碼段)總共有三種類型:git
window
對象,this
指向這個全局對象。Eval
函數執行上下文(eval代碼): 指的是運行在 eval
函數中的代碼,不多用並且不建議使用。執行上下文又包括三個生命週期階段:建立階段→執行階段→回收階段,本文重點介紹建立階段。github
1.建立階段瀏覽器
當函數被調用,但未執行任何其內部代碼以前,會作如下三件事:閉包
arguments
,提高函數聲明和變量聲明。後文會詳細說明。在一段 JS 腳本執行以前,要先解析代碼(因此說 JS 是解釋執行的腳本語言),解析的時候會先建立一個全局執行上下文環境,先把代碼中即將執行的變量、函數聲明都拿出來。變量先暫時賦值爲undefined
,函數則先聲明好可以使用。這一步作完了,而後再開始正式執行程序。函數
另外,一個函數在被執行以前,也會建立一個函數執行上下文環境,跟全局上下文差很少,不過函數執行上下文中會多出this
、 arguments
和函數的參數。ui
2.執行階段this
進入執行上下文、執行代碼spa
3.回收階段
執行完畢後執行上下文出棧並等待垃圾回收
假如咱們寫的函數多了,每次調用函數時都建立一個新的執行上下文,如何管理建立的那麼多執行上下文呢?
因此 JavaScript 引擎建立了執行上下文棧(Execution context stack,ECS)來管理執行上下文,具備 LIFO(後進先出)的棧結構,用於存儲在代碼執行期間建立的全部執行上下文。
首次運行JS代碼時,會建立一個全局執行上下文並Push到當前的執行棧中。每當發生函數調用,引擎都會爲該函數建立一個新的函數執行上下文並Push到當前執行棧的頂部,瀏覽器的JS執行引擎老是訪問棧頂的執行上下文。
根據執行棧LIFO規則,當棧頂函數運行完成後,其對應的函數執行上下文將會從執行棧中Pop出,上下文控制權將移到當前執行棧的下一個執行上下文,最終移回到全局執行上下文,全局上下文只有惟一的一個,它在瀏覽器關閉時Pop出。
看到目前爲止,是否以爲這兩個概念仍是有點晦澀難懂呢?那...接下來經過幾小段代碼和圖解來詳細介紹並理解吧。
讓咱們先來看一下這段簡單代碼:
function b(){
}
function a(){
b();
}
a();
複製代碼
這段代碼背後執行的邏輯是這樣的:
首先,全局執行上下文(Global Execution Context)會被創建,這時候會一併創建this
、global object
(window
),在函數開始執行的過程當中,function a
和b
因爲JS提高機制的緣故會先被創建在內存中,接着纔會開始逐行執行函數。
接着,代碼會執行到a( )
這個部分,這時候,會創建a
的執行上下文(execution context),而且被放置到執行棧(execution stack)中。在這個execution stack中,最上面的execution context會是正在被執行的a( )
。以下圖:
function a
的execution context創建後,便會開始執行function a
中的內容。因爲在function a( )
裏面有去執行function b
,所以,在這個execution stack中,接下來最上面會變成function b
的execution context。以下圖:
當function b
執行完以後,會從execution stack中離開,繼續逐行執行function a
。當function a
執行完以後,同樣會從execution stack中抽離,再回到Global Execution Context逐行執行。以下圖:
在瞭解了通常的函數其運做背後的邏輯後,讓咱們來看一下這段代碼:
function b(){
var myVar;
console.log(myVar);
}
function a(){
var myVar = 2;
b();
console.log(myVar);
}
var myVar = 1;
console.log(myVar);
a();
複製代碼
你能夠想像,若是咱們在不一樣的execution context中去把myVar
這個變量打出來,會獲得什麼結果呢?結果以下:
咱們分別獲得了一、undefined
和2。爲何會這樣呢?
讓咱們來看看這段代碼背後執行的邏輯:
首先,全局執行上下文(Global Execution Context)會被創建,因爲變量提高的緣故,myVar
、function a
和b
都會被創建並儲存在內存中,接着便開始逐行執行函數。一開始會碰到var myVar = 1
因此,最外層的myVar
便被給值爲1,接着執行到了console.log(myVar)
,這是在global execution context執行的,因而獲得了第一個1的結果:
而後執行到了a ( )
,因而創建了a
的execution context,這時候因爲逐行執行的關係,會先執行到var myVar = 2
,但由於這是在function a的execution context中,因此並不會影響到global execution context的myVar
:
在執行完function a
中的var myVar = 2
後,繼續逐行執行,因而執行到了b ( )
,這時候,function b
的execution function便被創建,並且會先去執行function b
裏面的內容:
function b
的execution function創建後,會開始逐行執行function b
裏面的內容,因而讀到了var myVar
;,這時候在function b
這個execution context中的myVar
變量被創建,可是還沒被賦值,因此會是undefined
。和上面提到的同樣,因爲這個myVar
是在function b
中的execution context所創建,因此並不會影響到其餘execution context的myVar
,這時候執行到了function b
的 execution context中的console.log(myVar)
,因而獲得了第二個看到的undefined
:
最後,function b
執行完以後,會從execution stack中離開,繼續回到function a
中的b( )
後逐行執行,也就是console.log(myVar)
,這時候是在function a的execution context加以執行的,所以也就獲得告終果中看到的第三個2了。
最後因爲b ( )
後面已經沒有內容,function a
執行完畢,這時候,function a
也會從execution stack中抽離。
最後回到Global Execution Context,若是函數中的a( )
後面還有內容的話,會繼續進行逐行執行。
由上面的例子,咱們能夠知道,咱們是在不一樣的execution context中分別去聲明變量myVar
的,所以在不一樣的execution context,變量彼此之間不會影響,因此雖然這三個變量都叫作myVar
,但實際上是三個不一樣的變量。
因爲咱們是在不一樣的execution context中去聲明變量,因此這實際上是位於三個不一樣execution context中的變量,因此即便咱們是在執行完a( )
後再去調用一次myVar
,同樣會獲得" 1"的結果:
function b(){
var myVar;
console.log(myVar);
}
function a(){
var myVar = 2;
b();
console.log(myVar);
}
var myVar = 1;
console.log(myVar);
a();
console.log(myVar); // 同樣會獲得"1"
複製代碼
最後須要注意的是,若是是在function
裏面直接使用myVar
這個變量,而沒有經過var
從新聲明它的話,就會獲得不一樣的結果!由於在函數做用域內加 var
定義的變量是局部變量,不加 var
定義的就成了全局變量。在未聲明新的變量的狀況下,在該execution context中JavaScript 引擎找不到這個變量,它就會往它的外層去尋找,最後會獲得,1 ,2 ,2 ,2 的結果:
function b(){
myVar;
console.log(myVar);
}
function a(){
myVar = 2;
b();
console.log(myVar);
}
var myVar = 1;
console.log(myVar);
a();
console.log(myVar);
/* 打印出 1 2 2 2 */
複製代碼
若是以爲文章對你有些許幫助,歡迎在個人GitHub博客點贊和關注,感激涕零!