順序執行?
如果要問到JavaScript程式碼執行順序的話,想必寫過JavaScript的開發者都會有個直觀的印象,那就是順序執行,畢竟
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
var foo = function () { console.log('foo1'); } foo(); // foo1 var foo = function () { console.log('foo2'); } foo(); // foo2 |
然而去看這段程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
function foo() { console.log('foo1'); } foo(); // foo2 function foo() { console.log('foo2'); } foo(); // foo2 |
列印的結果卻是兩個foo2。
刷過面試題的都知道這是因為JavaScript引擎並非一行一行地分析和執行程式,而是一段一段地分析執行。當執行一段程式碼的時候,會進行一個“準備工作”,比如第一個例子中的變數提升,和第二個例子中的函式提升。
但是本文真正想讓大家思考的是:這個”一段一段”中的“段”究竟是怎麼劃分的呢?
到底JavaScript引擎遇到一段怎樣的程式碼時才會做’準備工作’呢?
可執行程式碼
這就要說到JavaScript的可執行程式碼(executable code)的型別有哪些了?
其實很簡單,就三種,全域性程式碼、函式程式碼、eval程式碼。
舉個例子,當執行到一個函式的時候,就會進行準備工作,這裡的’準備工作’,讓我們用個更專業一點的說法,就叫做”執行上下文(execution contexts)”。
執行上下文棧
接下來問題來了,我們寫的函式多了去了,如何管理建立的那麼多執行上下文呢?
所以js引擎建立了執行上下文棧(Execution context stack,ECS)來管理執行上下文
為了模擬執行上下文棧的行為,讓我們定義執行上下文棧是一個陣列:
1 |
ECStack = []; |
試想當JavaScript開始要解釋執行程式碼的時候,最先遇到的就是全域性程式碼,所以初始化的時候首先就會向執行上下文棧壓入一個全域性執行上下文,讓我們用globalContext表示它,並且只有當整個應用程式結束的時候,ECStack才會被清空,所以ECStack最底部永遠有個globalContext:
1 2 3 |
ECStack = [ globalContext ]; |
現在JavaScript遇到下面的這段程式碼了:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
function fun3() { console.log('fun3') } function fun2() { fun3(); } function fun1() { fun2(); } fun1(); |
當遇到函式執行的時候,就會建立一個執行上下文,並且壓入執行上下文棧,當函式執行完畢的時候,就會將函式的執行上下文從棧中彈出。知道了這樣的工作原理,讓我們來看看如何處理上面這段程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
// 虛擬碼 // 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深入之詞法作用域和動態作用域》這篇文章最後的問題:
1 2 3 4 5 6 7 8 9 |
var scope = "global scope"; function checkscope(){ var scope = "local scope"; function f(){ return scope; } return f(); } checkscope(); |
1 2 3 4 5 6 7 8 9 |
var scope = "global scope"; function checkscope(){ var scope = "local scope"; function f(){ return scope; } return f; } checkscope()(); |
兩段程式碼執行的結果一樣,但是兩段程式碼究竟有哪些不同呢?
答案就是執行上下文棧的變化不一樣。
讓我們模擬第一段程式碼:
1 2 3 4 |
ECStack.push(checkscope> functionContext); ECStack.push(f> functionContext); ECStack.pop(); ECStack.pop(); |
讓我們模擬第二段程式碼:
1 2 3 4 |
ECStack.push(checkscope> functionContext); ECStack.pop(); ECStack.push(f> functionContext); ECStack.pop(); |
是不是有些不同呢?
當然,如果覺得這樣粗略的回答執行上下文棧的變化,依然顯得不夠詳細,那就讓我們去探究一下執行上下文到底包含了哪些內容,歡迎期待下一篇《JavaScript深入之變數物件》
深入系列
JavaScript深入系列預計寫十五篇左右,旨在幫大家捋順JavaScript底層知識,重點講解如原型、作用域、執行上下文、變數物件、this、閉包、按值傳遞、call、apply、bind、new、繼承等難點概念,與羅列它們的用法不同,這個系列更注重通過寫demo,捋過程、模擬實現,結合ES規範等方法來講解。
所有文章和demo都可以在github上https://github.com/mqyqingfeng/Blog找到。如果有錯誤或者不嚴謹的地方,請務必給予指正,十分感謝。如果喜歡或者有所啟發,歡迎star,對作者也是一種鼓勵。
本系列: