首發:https://www.love85g.com/?p=1723javascript
在這篇文章中,我將深刻研究JavaScript最基本的部分之一,即執行上下文。在這篇文章的最後,您應該更清楚地瞭解解釋器要作什麼,爲何在聲明一些函數/變量以前可使用它們,以及它們的值是如何肯定的。java
什麼是執行上下文?小程序
當代碼在JavaScript中運行時,執行它的環境是很是重要的,並被評估爲如下之一:瀏覽器
1:全局代碼——第一次執行代碼的默認環境。函數
2:函數代碼——每當執行流進入函數體時。this
3:要在內部Eval函數中執行的文本。spa
您能夠在線閱讀大量參考資料,其中涉及scope ,本文的目的是使事情更容易理解,讓咱們將術語 execution context(執行上下文) 看做當前代碼正在計算的環境/範圍。如今,討論得夠多了,讓咱們來看一個包含 global 和 function / local 上下文計算代碼的示例。線程
這裏沒什麼特別的,咱們有1個 global context 用紫色邊框表示,3個不一樣的function contexts 用綠色、藍色和橙色邊框表示。只能有一個 global context ,它能夠從程序中的任何其餘上下文訪問。指針
您能夠有任意數量的 function contexts ,而且每一個函數調用都建立一個新的上下文,該上下文建立一個私有範圍,其中函數內部聲明的任何內容都不能從當前函數範圍外部直接訪問。在上面的例子中,一個函數能夠訪問當前上下文以外聲明的變量,可是外部上下文不能訪問其中聲明的變量/函數。爲何會這樣?這段代碼到底是如何計算的?code
執行上下文堆棧
瀏覽器中的JavaScript解釋器是做爲一個線程實現的。這實際上意味着,在瀏覽器中,一次只能發生一件事,其餘操做或事件將排隊在所謂的執行堆棧中。下圖是單線程棧的抽象視圖:
咱們已經知道,當瀏覽器第一次加載腳本時,默認狀況下它會進入 global execution context 。若是在全局代碼中調用一個函數,程序的序列流將進入被調用的函數,建立一個新的 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 爲止。
關於執行堆棧,有5個關鍵點須要記住:
1:單線程的。
2:同步執行。
3:1個全局上下文。
4:無限的函數上下文。
5:每一個函數調用都會建立一個新的執行上下文,甚至是對自身的調用。
詳細執行上下文
如今咱們知道,每次調用一個函數,都會建立一個新的 execution context 。然而,在JavaScript解釋器中,對 execution context 的每一個調用都有兩個階段:
1:建立階段[當函數被調用,但在執行任何代碼以前]:
建立範圍鏈。
建立變量、函數和參數。
肯定「this」的值。
2:激活/代碼執行階段:
爲函數賦值、引用並解釋/執行代碼。
能夠將每一個 execution context (執行上下文)概念上表示爲一個具備3個屬性的對象:
executionContextObj = { 'scopeChain': { /* 變量對象+全部父執行上下文的變量對象 */ }, 'variableObject': { /* 函數參數/參數,內部變量和函數聲明 */ }, 'this': {} }
激活/變量對象[AO/VO]
這個 executionContextObj 在調用函數時建立,但在實際函數執行以前建立。這被稱爲階段1,建立階段。在這裏,解釋器經過掃描函數尋找傳入的參數或參數、局部函數聲明和局部變量聲明來建立executionContextObj 。該掃描的結果成爲executionContextObj 中的variableObject。
下面是解釋器如何評估代碼的僞概述:
找到一些代碼來調用函數。
在執行函數代碼以前,建立執行上下文。
進入創做階段:
初始化範圍鏈。
建立變量對象:
建立arguments對象,檢查參數上下文,初始化名稱和值,並建立引用副本。
掃描上下文中的函數聲明:
對於找到的每一個函數,在變量對象中建立一個屬性,該屬性是確切的函數名,該函數在內存中有一個指向該函數的引用指針。
若是函數名已經存在,則重寫引用指針值。
掃描上下文變量聲明:
對於找到的每一個變量聲明,在變量對象中建立一個屬性,即變量名,並初始化值爲undefined。
若是變量名已經存在於變量對象中,則什麼也不作,繼續掃描。
肯定上下文中「this」的值。
激活/代碼執行階段:
在上下文中運行/解釋函數代碼,並在逐行執行代碼時分配變量值。
讓咱們來看一個例子:
function foo(i) { var a = 'hello'; var b = function privateB() { }; function c() { } } foo(22);
調用foo(22)時,建立階段以下:
fooExecutionContext = { scopeChain: { ... }, variableObject: { arguments: { 0: 22, length: 1 }, i: 22, c: pointer to function c() a: undefined, b: undefined }, this: { ... } }
如您所見,建立階段處理定義屬性的名稱,而不是爲它們賦值,只有形式參數/參數例外。建立階段完成後,執行流程進入函數,函數完成執行後,激活/代碼執行階段以下:
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聲明瞭兩次,爲何Foo是函數而不是未定義或字符串?
儘管foo聲明瞭兩次,但從建立階段咱們就知道函數是在變量以前在激活對象上建立的,若是激活對象上的屬性名已經存在,那麼咱們只需繞過解密。
所以,首先在激活對象上建立對函數foo()的引用,當解釋器到達var foo時,咱們已經看到了屬性名foo的存在,因此代碼什麼也不作,繼續執行。
爲何bar沒有定義?
bar其實是一個具備函數賦值的變量,咱們知道這些變量是在建立階段建立的,可是它們是用undefined值初始化的。
總結
但願如今您已經很好地理解了JavaScript解釋器是如何評估代碼的。理解執行上下文和堆棧可讓您瞭解代碼爲何要計算您最初沒有預料到的不一樣值的緣由。
您是否定爲了解解釋器的內部工做方式對您的JavaScript知識來講是太大的開銷仍是必需的?瞭解執行上下文階段是否有助於編寫更好的JavaScript ?
原文:http://davidshariff.com/blog/...
歡迎關注小程序,感謝您的支持!