這篇文章中,我將深刻探討JavaScript中的一個最基本的部分,即執行上下文(或稱環境)。讀過本文後,你將更加清楚地瞭解到解釋器嘗試作什麼,爲何在聲明某些函數/變量以前,可使用它們以及它們的值是如何肯定的。javascript
在運行JavaScript代碼時,執行環境很是重要,並能夠認爲是如下其中之一:java
你能夠在網上查到大量的關於scope(做用域)
的資料,本文的目的就是要讓事情更加容易理解。咱們把術語執行上下文
視爲當前代碼的評估環境/範圍。如今,條件充足,咱們看個包含全局和函數/本地
上下文評估代碼的示例。git
這裏沒什麼特別的,咱們有1個由紫色邊框表示的全局上下文
和由綠色、藍色和橙色邊框表示的3個不一樣的函數上下文
。只有1個全局上下文,咱們能夠從程序的任何其它上下文訪問。github
你能夠擁有任意數量的函數上下文
,而且每一個函數調用都會建立一個新的上下文,從而建立一個私有的做用域,沒法從當前函數做用域外直接訪問函數內部聲明的任何內容。在上面的例子中,函數能夠訪問在其當前上下文以外聲明的變量,可是外部上下文沒法訪問(函數)其中聲明的變量/函數。爲何會這樣?這段代碼到底是如何評估的?瀏覽器
瀏覽器中的JavaScript解釋器是單線程實現的。這意味着在瀏覽器中一次只能發生一件事情,其它動做或事件在所謂的執行棧
中排隊。下圖是單線程棧的抽象視圖:閉包
咱們知道,當瀏覽器首次加載腳本時,它默認進入全局執行上下文
。若是在全局代碼中調用一個函數,程序的順序流就進入被調用的函數,建立一個新的執行上下文
並將該上下文推送到執行棧
的頂部。ecmascript
若是你在當前函數中調用另一個函數,則會發生一樣的事情。代碼的執行流程進入函數內部,該函數建立一個新的執行上下文
,該上下文被推送到現有棧的頂部。瀏覽器將始終執行位於棧頂部的當前執行上下文
,而且一旦函數完成當前執行上下文
,它將從棧頂彈出,將控制權返回當前棧的棧頂上下文。下面的例子展現了遞歸函數和其程序的執行棧
:函數
(function foo(i) {
if (i === 3) {
return;
}
else {
foo(++i);
}
}(0));
複製代碼
上面代碼只調用自身3次,將i的值遞增1。每次調用函數foo
時,都會建立一個新的執行上下文。一旦上下文執行完畢,它就會彈出棧而且將控制權返回它下面的上下文,直到再次到達全局上下文
。ui
關於執行棧
有五個關鍵點:this
執行上下文
,甚至是調用自身因此,咱們如今知道每次調用一個函數時,都會建立一個新的執行上下文
。可是,在JavaScript的解釋器中,執行上下文
的調用都有兩個階段:
能夠將每一個執行上下文
在概念上標示爲具備3個屬性的對象:
executionContextObj = {
'scopeChain': { /* variableObject + all parent execution context's variableObject */ },
'variableObject': { /* function arguments / parameters, inner variable and function declarations */ },
'this': {}
}
複製代碼
調用函數時,但在執行實際函數以前,會建立此executionContextObj
。這被稱爲階段1,即建立階段
。這裏,解釋器經過掃描傳入的參數或參數的函數、本地函數聲明和局部函數聲明來建立executionContextObj
。此掃描的結果將稱爲executionContextObj
中的variableObject
。
如下是解釋器如何評估代碼的僞概述:
函數
代碼以前,建立執行上下文
。做用域鏈
變量對象
:
arguments對象
,檢查參數的上下文,初始化名稱和值並建立引用的副本。變量對象(或活動對象)
中建立一個屬性,該屬性是確切的函數名稱,該函數具備指向內存中函數的引用指針。變量對象(或活動對象)
中建立一個屬性,該屬性是變量名稱,並將值初始化爲undefined。變量對象(或活動對象)
中,則不執行任何操做並繼續掃描(即跳過)。看下下面的例子:
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術語-提高的資源,解釋變量和函數聲明是否被提高到其功能範圍的頂部。可是,沒有人詳細解釋爲何會發生這種狀況,在掌握了關於解釋器如何建立活動對象的新知識點,就很容易理解爲何了。看下下面的代碼例子:
(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顯示爲函數而不是undefined
或string
呢?
foo
被聲明瞭兩次,咱們從建立階段
中就知道到達變量以前在活動對象
上已經建立了函數,而且若是活動對象
上已經存在屬性名稱,咱們就會繞過了聲明。foo()
的引用,而且當解釋器到達var foo
時,咱們已經看到名稱foo
存在,所以代碼什麼都不作而且繼續。爲何bar是undefined
?
bar
其實是一個具備函數賦值的變量,咱們知道變量是在建立階段
建立的,但它們是使用undefined
值初始化的。但願到如今,你已經很好地掌握了JavaScript解釋器是如何評估你的代碼。理解執行上下文和環境棧可讓你瞭解代碼的評估和你預期不一樣值的緣由。
你是認爲了解解釋器的內部工做原理是多餘的仍是必要的JavaScript知識點呢?知道執行上下文是否有助你編寫出更好的JavaScript?
筆記:有些人一直在詢問閉包,回調,timeout等知識點,我將在下一篇文章中介紹,更多地關注與執行環境
相關的做用域鏈。
原文: http://davidshariff.com/blog/what-is-the-execution-context-in-javascript/
文章首發:https://github.com/reng99/blogs/issues/11
更多內容:https://github.com/reng99/blogs