JavaScript系列之執行上下文和執行棧

若是你想成爲一名優秀的JavaScript 開發者,那你就必須瞭解 JavaScript 程序內部的執行機制。而執行上下文和執行棧是其關鍵概念之一, 理解執行上下文和執行棧一樣有助於理解其餘的 JavaScript 概念如提高機制、做用域和閉包等。javascript

執行上下文和執行棧是JavaScript的難點之一,因此本人儘可能用通俗易懂的方式來闡述這些概念。java

執行上下文(Execution Context)

當 JavaScript 代碼執行一段可執行代碼(executable code)時,會建立對應的執行上下文(execution context)。執行上下文(可執行代碼段)總共有三種類型:git

  • 全局執行上下文(全局代碼):不在任何函數中的代碼都位於全局執行上下文中,只有一個,瀏覽器中的全局對象就是 window 對象,this 指向這個全局對象。
  • 函數執行上下文(函數體):只有調用函數時,纔會爲該函數建立一個新的執行上下文,能夠存在無數個,每當一個新的執行上下文被創-建,它都會按照特定的順序執行一系列步驟。
  • Eval 函數執行上下文(eval代碼): 指的是運行在 eval 函數中的代碼,不多用並且不建議使用。

執行上下文又包括三個生命週期階段:建立階段→執行階段→回收階段,本文重點介紹建立階段。github

1.建立階段瀏覽器

當函數被調用,但未執行任何其內部代碼以前,會作如下三件事:閉包

  • 建立變量對象(Variable object,VO):首先初始化函數的參數arguments,提高函數聲明和變量聲明。後文會詳細說明。
  • 建立做用域鏈(Scope Chain):在執行上下文的建立階段,做用域鏈是在變量對象以後建立的。做用域鏈自己包含變量對象。做用域鏈用於解析變量。當被要求解析變量時,JavaScript 始終從代碼嵌套的最內層開始,若是最內層沒有找到變量,就會跳轉到上一層父做用域中查找,直到找到該變量。後文會詳細說明。
  • 肯定this指向:包括多種狀況,後文會詳細說明。

在一段 JS 腳本執行以前,要先解析代碼(因此說 JS 是解釋執行的腳本語言),解析的時候會先建立一個全局執行上下文環境,先把代碼中即將執行的變量、函數聲明都拿出來。變量先暫時賦值爲undefined,函數則先聲明好可以使用。這一步作完了,而後再開始正式執行程序。函數

另外,一個函數在被執行以前,也會建立一個函數執行上下文環境,跟全局上下文差很少,不過函數執行上下文中會多出thisarguments和函數的參數。ui

2.執行階段this

進入執行上下文、執行代碼spa

3.回收階段

執行完畢後執行上下文出棧並等待垃圾回收

執行上下文棧(Execution Context Stack)

假如咱們寫的函數多了,每次調用函數時都建立一個新的執行上下文,如何管理建立的那麼多執行上下文呢?

因此 JavaScript 引擎建立了執行上下文棧(Execution context stack,ECS)來管理執行上下文,具備 LIFO(後進先出)的棧結構,用於存儲在代碼執行期間建立的全部執行上下文。

首次運行JS代碼時,會建立一個全局執行上下文並Push到當前的執行棧中。每當發生函數調用,引擎都會爲該函數建立一個新的函數執行上下文並Push到當前執行棧的頂部,瀏覽器的JS執行引擎老是訪問棧頂的執行上下文。

根據執行棧LIFO規則,當棧頂函數運行完成後,其對應的函數執行上下文將會從執行棧中Pop出,上下文控制權將移到當前執行棧的下一個執行上下文,最終移回到全局執行上下文,全局上下文只有惟一的一個,它在瀏覽器關閉時Pop出。

看到目前爲止,是否以爲這兩個概念仍是有點晦澀難懂呢?那...接下來經過幾小段代碼和圖解來詳細介紹並理解吧。

執行上下文是如何執行的呢?

讓咱們先來看一下這段簡單代碼:

function b(){
}
function a(){
  b();
}
a();
複製代碼

這段代碼背後執行的邏輯是這樣的:

首先,全局執行上下文(Global Execution Context)會被創建,這時候會一併創建thisglobal object (window),在函數開始執行的過程當中,function ab因爲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)會被創建,因爲變量提高的緣故,myVarfunction ab都會被創建並儲存在內存中,接着便開始逐行執行函數。一開始會碰到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博客點贊和關注,感激涕零!

相關文章
相關標籤/搜索