在這篇文章中,我將深刻探討JavaScript的一個最基本的部分,執行上下文。 在本文結束時,您會更清楚解釋器都作了些什麼,以致於某些函數、變量在聲明它們以前就可使用,它們的值是如何肯定的。
當代碼在JavaScript中運行時,它的執行環境很是重要,而且它們分爲如下幾類:javascript
爲了便於理解,本文中執行上下文是指:當前被執行的代碼的環境、做用域;接下來讓咱們看一個執行上下文中包含global、function content的代碼:java
這裏沒有什麼特別之處,1個global context由紫色邊框表示,3個不一樣的function contexts分別由綠色、藍色和橙色邊框表示。只能有1個global context,能夠從程序中的任何其餘上下文訪問。git
您能夠擁有任意數量的function contexts,而且每一個函數調用都會建立一個新的上下文,從而建立一個私有做用域,在該做用域內,沒法從當前函數做用域外直接訪問函數內部聲明的任何內容。在上面的示例中,函數能夠訪問在其當前上下文以外聲明的變量,但外部上下文沒法訪問在其內部聲明的變量/函數。爲何會這樣?這段代碼到底是如何運行的?github
瀏覽器中的JavaScript解釋器單線程運行。這就意味着同一時間瀏覽器只執行一件事,其它的事件在執行隊列中排隊。下圖是單線程隊列的抽象視圖:面試
咱們已經知道,當瀏覽器首次加載您的腳本時,它默認進入全局執行上下文(global execution contenrt)。若是在您的全局代碼中調用一個函數,程序的順序流進入被調用的函數,建立一個新函數execution context並將該上下文推送到頂部execution stack(執行隊列)。瀏覽器
若是在當前函數中調用另外一個函數,則會發生一樣的事情。代碼的執行流程進入內部函數,該函數建立一個execution context並推送到執行隊列的頂部。瀏覽器始終執行位於堆棧頂部的execution context,而且一旦函數完成執行當前操做execution context,它將從堆棧頂部彈出,將控制權返回到當前堆棧中的下方上下文。下面的例子顯示了一個遞歸函數和程序execution stack:閉包
(function foo(i) { if (i === 3) { return; } else { foo(++i); } }(0));
代碼只調用自身3次,將i的值遞增1.每次調用foo函數時,都會建立一個新的執行上下文。一旦執行完成,它就會彈出堆棧而且將控制權交給它下面的上下文,直到再次到達global context(koa2的洋蔥圖想到了沒?)koa
因此咱們如今知道每次調用函數時都會建立一個新的執行上下文(execution context) 。可是,在JavaScript解釋器中,每次調用生成執行上下文(execution context)都有兩個階段:ecmascript
- 建立做用域鏈。
- 建立變量(variables),函數(functions )和參數(arguments)
- 肯定"this"。
- var 賦值,(function聲明)指向函數,解釋/執行代碼
能夠將每一個execution context概念上表示爲具備3個屬性的對象:函數
executionContextObj = { 'scopeChain': { /* variableObject + all parent execution context's variableObject */ }, 'variableObject': { /* function arguments / parameters, inner variable and function declarations */ }, 'this': {} }
這executionContextObj是在調用函數時,但在執行實際函數以前建立的。這是第一階段:建立階段。這裏,解釋器經過掃描傳入的參數或arguments、本地函數聲明和局部變量聲明來建立executionContextObj。此次掃描的結果就變成了executionContextObj.variableObject。
進入建立階段:
建立變量對象(variable object):
掃描上下文以獲取函數聲明:
掃描上下文以獲取變量聲明:
激活/執行階段:
咱們來看一個例子:
function foo(i) { var a = 'hello'; var b = function privateB() { }; function c() { } } foo(22);
在調用時foo(22),creation stage長這樣子:
fooExecutionContext = { scopeChain: { ... }, variableObject: { arguments: { 0: 22, length: 1 }, i: 22, c: pointer to function c() a: undefined, b: undefined }, this: { ... } }
正如您所看到的,creation stage定義屬性的name,不爲它們賦值,但formal arguments / parameters(函數傳參,arguments)除外。一旦creation stage完成後,執行流程進入函數體,在函數已經完成執行以後的execution stage以下:
fooExecutionContext = { scopeChain: { ... }, variableObject: { arguments: { 0: 22, length: 1 }, i: 22, c: pointer to function c() a: 'hello', b: pointer to function privateB() }, this: { ... } }
在不少JavaScript的資料中都提到了提高,解釋變量和函數聲明被提高到其做用域的頂部。可是,沒有人詳細解釋爲何會發生這種狀況,而在你掌握了關於解釋器如何建立activation object後,會很容易理解。示例:
(function() { console.log(typeof foo); // function pointer console.log(typeof bar); // undefined var foo = 'hello', bar = function() { return 'world'; }; function foo() { return 'hello'; } }());
咱們如今能夠回答的問題是:
爲何咱們能夠在聲明它以前訪問foo?
foo是聲明瞭兩次,爲何顯示foo的是 function ,__不是__ undefined 或 string?
爲何 bar 是 undefined?
但願到如今您已經很好地掌握了JavaScript解釋器如何執行您的代碼。理解執行上下文和隊列可讓您瞭解代碼沒有達到預期的緣由
您是否定爲了解解釋器的內部工做原理是您的JavaScript知識的重要組成部分?知道執行上下文的每一個階段是否有助於您編寫更好的JavaScript?
__注意__:有些人一直在問關於閉包,回調,超時等,我將在在下一篇文章中涉及,主要概述做用域鏈與execution context的關係。