翻譯:瘋狂的技術宅
連接: http://davidshariff.com/blog/...
本文首發微信公衆號:jingchengyideng
歡迎關注,天天都給你推送新鮮的前端技術文章javascript
在這篇文章中,我將深刻探討JavaScript的最基本部分之一,即Execution Context
(執行上下文)。 在本文結束時,你應該對解釋器瞭解得更清楚:爲何在聲明它們以前可使用某些函數或變量?以及它們的值是如何肯定的?前端
JavaScript的執行環境很是重要,當JavaScript代碼在行時,會被預處理爲如下狀況之一:java
你能夠閱讀大量涉及做用域
的在線資料,不過爲了使事情更容易理解,讓咱們將術語「執行上下文」
視爲當前代碼的運行環境或做用域。接下來讓咱們看一個包含global和function / local上下文的代碼示例。瀏覽器
這裏沒有什麼特別之處,咱們有一個由紫色邊框表示的全局上下文
,和由綠色,藍色和橙色邊框表示的3個不一樣的函數上下文
。 只能有1個全局上下文
,能夠從程序中的任何其餘上下文訪問。微信
你能夠擁有任意數量的函數上下文
,而且每一個函數調用都會建立一個新的上下文,從而建立一個私有做用域,其中沒法從當前函數做用域外直接訪問函數內部聲明的任何內容。 在上面的示例中,函數能夠訪問在其當前上下文以外聲明的變量,但外部上下文沒法訪問在其中聲明的變量或函數。 爲何會這樣呢? 這段代碼到底是如何處理的?ecmascript
瀏覽器中的JavaScript解釋器被實現爲單個線程。 實際上這意味着在瀏覽器中一次只能作一件事,其餘動做或事件在所謂的執行堆棧中排隊。 下圖是單線程堆棧的抽象視圖:ide
咱們已經知道,當瀏覽器首次加載腳本時,它默認進入全局上下文
執行。 若是在全局代碼中調用函數,程序的順序流進入被調用的函數,建立新的執行上下文並將其推送到執行堆棧
的頂部。函數
若是在當前函數中調用另外一個函數,則會發生一樣的事情。 代碼的執行流程進入內部函數,該函數建立一個新的執行上下文
,該上下文被推送到現有堆棧的頂部。 瀏覽器將始終執行位於堆棧頂部的當前執行上下文
,而且一旦函數執行完當前執行上下文
後,它將從棧頂部彈出,把控制權返回到當前棧中的下一個上下文。 下面的示例顯示了遞歸函數和程序的執行堆棧
:學習
(function foo(i) { if (i === 3) { return; } else { foo(++i); } }(0));
代碼簡單地調用自身3次,並將i
的值遞增1。每次調用函數foo
時,都會建立一個新的執行上下文
。 一旦上下文完成執行,它就會彈出堆棧而且講控制返回到它下面的上下文,直到再次達到全局上下文
。this
關於執行堆棧execution stack
有5個關鍵要點:
execution context
,甚至是對自身的調用。因此咱們如今知道每次調用一個函數時,都會建立一個新的執行上下文
。 可是,在JavaScript解釋器中,對執行上下文
的每次調用都有兩個階段:
建立階段 [調用函數時,但在執行任何代碼以前]:
做用域鏈
。「this」
的值。激活/代碼執行階段:
能夠將每一個執行上下文
在概念上表示爲具備3個屬性的對象:
executionContextObj = { 'scopeChain': { /* variableObject + 全部父執行上下文的variableObject */ }, 'variableObject': { /* 函數實參/形參,內部變量和函數聲明 */ }, 'this': {} }
在調用該函數,而且在實際執行函數以前,會建立這個executionContextObj
。 這被稱爲第1階段,即創造階段
。 這時解釋器經過掃描函數傳遞的實參或形參、本地函數聲明和局部變量聲明來建立executionContextObj
。 此掃描的結果將成爲executionContextObj
中的variableObject
。
如下是解釋器如何預處理代碼的僞代碼概述:
執行上下文
。進入建立階段:
建立variable object
:
arguments object
,檢查參數的上下文,初始化名稱和值並建立引用副本。掃描上下文以獲取函數聲明:
variable object
中建立一個屬性,該屬性是函數的確切名稱,該屬性存在指向內存中函數的引用指針。掃描上下文以獲取變量聲明:
variable object
中建立一個屬性做爲變量名稱,並將該值初始化爲undefined
。variable object
中,則不執行任何操做並繼續掃描。「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定義術語hoisting
的在線資源,解釋變量和函數聲明被hoisting到其函數範圍的頂部。 可是沒有人可以詳細解釋爲何會發生這種狀況,掌握了關於解釋器如何建立激活對象
的新知識,很容易理解爲何。 請看下面的代碼示例:
(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顯示爲function
而不是undefined
或string
?
foo
被聲明兩次,咱們經過建立階段
知道函數在變量以前就被建立在激活對象
上了,並且若是激活對象
上已經存在了屬性名稱,咱們只是繞過了聲明這一步驟。激活對象
上建立對函數foo()
的引用,而且當解釋器到達var foo
時,咱們已經看到屬性名稱foo
存在,所以代碼不執行任何操做並繼續處理。爲何bar
未定義?
bar
其實是一個具備函數賦值的變量,咱們知道變量是在建立階段
被建立的,但它們是使用undefined
值初始化的。但願到這裏你已經可以很好地掌握了JavaScript解釋器如何預處理你的代碼。 理解執行上下文和堆棧可讓你瞭解背後的緣由:爲何代碼預處理後的值和你預期的不同。
你認爲學習解釋器的內部工做原理是畫蛇添足仍是很是必要的呢? 瞭解執行上下文階段是否可以幫你你寫出更好的JavaScript呢?
歡迎掃描二維碼關注公衆號,天天都給你推送新鮮的前端技術文章