JavaScript深入之執行上下文棧

冴羽發表於2017-04-10

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複製程式碼

列印的結果卻是兩個 foo2

刷過面試題的都知道這是因為 JavaScript 引擎並非一行一行地分析和執行程式,而是一段一段地分析執行。當執行一段程式碼的時候,會進行一個“準備工作”,比如第一個例子中的變數提升,和第二個例子中的函式提升。

但是本文真正想讓大家思考的是:這個“一段一段”中的“段”究竟是怎麼劃分的呢?

到底JavaScript引擎遇到一段怎樣的程式碼時才會做“準備工作”呢?

可執行程式碼

這就要說到 JavaScript 的可執行程式碼(executable code)的型別有哪些了?

其實很簡單,就三種,全域性程式碼、函式程式碼、eval程式碼。

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

執行上下文棧

接下來問題來了,我們寫的函式多了去了,如何管理建立的那麼多執行上下文呢?

所以 JavaScript 引擎建立了執行上下文棧(Execution context stack,ECS)來管理執行上下文

為了模擬執行上下文棧的行為,讓我們定義執行上下文棧是一個陣列:

    ECStack = [];複製程式碼

試想當 JavaScript 開始要解釋執行程式碼的時候,最先遇到的就是全域性程式碼,所以初始化的時候首先就會向執行上下文棧壓入一個全域性執行上下文,我們用 globalContext 表示它,並且只有當整個應用程式結束的時候,ECStack 才會被清空,所以 ECStack 最底部永遠有個 globalContext:

    ECStack = [
        globalContext
    ];複製程式碼

現在 JavaScript 遇到下面的這段程式碼了:

function fun3() {
    console.log('fun3')
}

function fun2() {
    fun3();
}

function fun1() {
    fun2();
}

fun1();複製程式碼

當執行一個函式的時候,就會建立一個執行上下文,並且壓入執行上下文棧,當函式執行完畢的時候,就會將函式的執行上下文從棧中彈出。知道了這樣的工作原理,讓我們來看看如何處理上面這段程式碼:

// 虛擬碼

// fun1()
ECStack.push(<fun1> functionContext);

// fun1中竟然呼叫了fun2,還要建立fun2的執行上下文
ECStack.push(<fun2> functionContext);

// 擦,fun2還呼叫了fun3!
ECStack.push(<fun3> functionContext);

// fun3執行完畢
ECStack.pop();

// fun2執行完畢
ECStack.pop();

// fun1執行完畢
ECStack.pop();

// javascript接著執行下面的程式碼,但是ECStack底層永遠有個globalContext複製程式碼

解答思考題

好啦,現在我們已經瞭解了執行上下文棧是如何處理執行上下文的,所以讓我們看看上篇文章《JavaScript深入之詞法作用域和動態作用域》最後的問題:

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();複製程式碼

是不是有些不同呢?

當然了,這樣概括的回答執行上下文棧的變化不同,是不是依然有一種意猶未盡的感覺呢,為了更詳細講解兩個函式執行上的區別,我們需要探究一下執行上下文到底包含了哪些內容,所以歡迎閱讀下一篇《JavaScript深入之變數物件》。

下一篇文章

《JavaScript深入之變數物件》

深入系列

JavaScript深入系列目錄地址:github.com/mqyqingfeng…

JavaScript深入系列預計寫十五篇左右,旨在幫大家捋順JavaScript底層知識,重點講解如原型、作用域、執行上下文、變數物件、this、閉包、按值傳遞、call、apply、bind、new、繼承等難點概念。

如果有錯誤或者不嚴謹的地方,請務必給予指正,十分感謝。如果喜歡或者有所啟發,歡迎star,對作者也是一種鼓勵。

相關文章