深入學習js之——執行上下文棧#3

MagicalLouis發表於2019-02-17

深入學習js系列是自己階段性成長的見證,希望通過文章的形式更加嚴謹、客觀地梳理js的相關知識,也希望能夠幫助更多的前端開發的朋友解決問題,期待我們的共同進步。

如果覺得本系列不錯,歡迎點贊、評論、轉發,您的支援就是我堅持的最大動力。


開篇

作為一個JavaScript的程式開發者,如果被問到JavaScript程式碼的執行順序,你腦海中是不是有一個直觀的印象 -- JavaScript 是順序執行的,可事實真的是這樣的嗎?

讓我們首先看兩個小例子:

var foo = function () {
  console.log('foo1');
}

foo();  // foo1

var foo = function () {
  console.log('foo2');
}

foo(); // foo2
複製程式碼
function foo() {
  console.log('foo1');
}

foo();  // foo2

function foo() {
  console.log('foo2');
}

foo(); // foo2
複製程式碼

刷過面試題目的都知道:

JavaScript引擎並非一行一行地分析和執行程式,而是一段一段地分析執行,當執行一段程式碼的時候,會進行一個準備工作。

比如我們熟悉的JavaScript中的變數提升比如函式提升都是在這個準備階段完成的。

本文我們就來深入的研究一下,這一段一段中的是如何劃分的呢?

到底JavaScript引擎遇到一段怎樣的程式碼才會做"準備工作"呢?為了解答這個問題我們引入一個概念——執行上下文

執行上下文

如果你做過小學的閱讀理解,肯定見到過這樣的題目:聯絡上下文解釋句子,這裡的上下文指的可能是這個句子所在的段落,也可能是這個句子所在段落的臨近段落。實際上,這裡描述的是一個句子的語境和作用範圍,聯絡類比到程式中我們可以作如下定義:

執行上下文是當前JavaScript程式碼被解析和執行時所在環境的抽象概念。

執行上下文的型別

執行上下文總共分為三種型別,有時候我們也叫做可執行程式碼(executable code)

  • 全域性執行上下文: 只有一個,瀏覽器中的全域性物件就是window物件,this指向這個全域性物件。
  • 函式執行上下文: 存在無數個,只有在函式被呼叫的時候才會被建立,每次呼叫函式都會建立一個新的執行上下文。
  • Eval 函式執行上下文: 指的是執行在eval函式中的程式碼,很少用而且不建議使用。

舉個例子,當執行到一個函式的時候,就會進行準備工作,這裡的"準備工作",讓我們用個更專業一點的說法,就叫做"執行上下文(execution context)"。

執行棧

接下來問題來了,我們寫的函式多了去了,如何管理建立的那麼多執行上下文呢?所以 JavaScript 引擎建立了執行上下文棧(Execution context stack )ECStack 來管理執行上下文。

這裡我們可以簡單的認為 ECStack 是一個陣列,類似這樣:

ECStack = [];
複製程式碼

執行棧,也叫做呼叫棧,具有 LIFO(last in first out 後進先出) 結構,用於儲存在程式碼執行期間建立的所有執行上下文。

  • 首次執行JavaScript程式碼的時候,會建立一個全域性執行的上下文並Push到當前的執行棧中,每當發生函式呼叫,引擎都會為該函式建立一個新的函式執行上下文並Push當前執行棧的棧頂。
  • 當棧頂的函式執行完成後,其對應的函式執行上下文將會從執行棧中Pop出,上下文的控制權將移動到當前執行棧的下一個執行上下文。

讓我們看一段程式碼來理解這個過程:

var 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');

// Inside first function
// Inside second function
// Again inside first function
// Inside Global Execution Context
複製程式碼
  • 當上述程式碼在瀏覽器載入時,JavaScript引擎建立了一個全域性執行上下文並把它壓入(push) 當前的執行棧。當遇到 first() 函式呼叫時,JavaScript引擎為該函式建立一個新的執行上下文並把它壓入當前執行棧的頂部
  • 當從 first() 函式內部呼叫 second() 函式時,JavaScript引擎為 second() 函式建立了一個新的執行上下文並把它壓入當前執行棧的頂部,當 second() 函式執行完畢,它的執行上下文會從當前棧彈出(pop),並且控制流程到達下一個執行上下文,即 first() 函式的執行上下文。
  • first() 執行完畢,它的執行上下文從棧中彈出,控制流程到達了全域性執行上下文。一旦所有的程式碼執行完畢,JavaScript引擎從當前棧中移出全域性執行上下文。

下面這張圖,能夠更加清晰的解釋上面這個執行過程

深入學習js之——執行上下文棧#3

看兩個思考題

var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f();
}
checkscope();
複製程式碼
var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f;
}
checkscope()();
複製程式碼

兩段程式碼執行的結果一樣,但是兩段程式碼究竟有哪些不同呢?

答案就是執行上下文棧的變化不一樣。

讓我們模擬第一段程式碼:

ECStack.push(<checkscope> functionContext);
ECStack.push(<f> functionContext);
ECStack.pop();
ECStack.pop();
複製程式碼

讓我們模擬第二段程式碼:

ECStack.push(<checkscope> functionContext);
ECStack.pop();
ECStack.push(<f> functionContext);
ECStack.pop();
複製程式碼

checkscope()();這裡對於這個函式的執行做一些解釋

// checkscope()() 就相當於
var f = checkscope();
f();
複製程式碼

checkscope 函式執行,函式執行完畢後,該函式返回一個函式名,就相當於:

ECStack.push(<checkscope> functionContext);
ECStack.pop();
複製程式碼

然後再執行的這個返回的函式,就相當於:

ECStack.push(<f> functionContext);
ECStack.pop();
複製程式碼

為了更詳細講解兩個函式執行上的區別,我們需要探究一下執行上下文到底包含了哪些內容,我們需要更加深入瞭解變數物件的相關內容。

參考連結:

《理解 JavaScript 中的執行上下文和執行棧》

《JavaScript深入之執行上下文棧》

深入學習JavaScript系列目錄

歡迎新增我的個人微信討論技術和個體成長。

深入學習js之——執行上下文棧#3
歡迎關注我的個人微信公眾號——指尖的宇宙,更多優質思考乾貨

深入學習js之——執行上下文棧#3

相關文章