本文翻譯之 http://davidshariff.com/blog/what-is-the-execution-context-in-javascript/
在這篇文章中,我將深入探討JavaScript中一個最基本的部分,即Execution Context。 在本文結束時,您應該更清楚地知道解釋器是怎么工作的,為什么某些函數/變量在聲明之前就可以使用以及它們的值是如何確定的。
一:什么是執行上下文?
當JavaScript代碼運行的時候,確定它運行所在的環境是非常重要的。運行環境由下面三種不同的代碼類型確定
- 全局代碼(Global Code):代碼首次執行時候的默認環境
- 函數代碼(Function Code):每當執行流程進入到一個函數體內部的時候
- Eval代碼(Eval Code):當eval函數內部的文本執行的時候
您可以在網上找到大量關于scope
的參考資料。為了更易于理解,我們將execution context簡單視為運行當前代碼的environment/scope。好了,話不多說,先讓我們看個例子,其中包含了global context和function/local context 代碼。
在上圖中,我們有1個全局上下文(Global Context),使用紫色邊框表示;有3個不同的函數上下文(Function Context)由綠色,藍色,和橙色邊框表示。注意!全局上下文有且只有一個,程序中其他任意的上下文都可以訪問全局上下文。
你可以擁有任意數量的函數上下文。每一次函數調用都會創建一個新的上下文,它會創建一個私有域,函數內部做出的所有聲明都會放在這個私有域中,并且這些聲明在當前函數作用域外無法直接訪問。在上面的例子中,一個函數可以訪問它所在的上下文尾部的變量,但是一個外部的上下文無法訪問內部函數內部聲明的變量/函數。為什么會發生這樣的情況?代碼究竟是如何被解析的呢?
二:執行上下文棧
瀏覽器中的JS解釋器是單線程的。也就是說在瀏覽器中同一時間只能做一個事情,其他的action和event都會被排隊放入到執行棧中(Execution Stack)。下圖表示了一個單線程棧的抽象視圖
如我們所知,當一個瀏覽器第一次load你的代碼的時候,首先它會進入到一個全局執行上下文中。如果在你的全局代碼中,你調用了一個函數,那么程序的執行流程會進入到被調用的函數中,并創建一個新的執行上下文,并將這個上下文推入到執行棧頂。
如果在當前的函數中,你由調用了一個函數,那么也會執行同樣的操作。執行流程計入到剛被調用的函數內部,重新創建一個新的執行上下文,并再次推入到執行棧頂。瀏覽器會一直執行當前棧頂的執行上下文,一旦函數執行完畢,該上下文就會被推出執行棧。下面的例子展示了一個遞歸函數以及該程序的執行棧:
(function foo(i) {
if (i === 3) {
return;
}
else {
foo(++i);
}
}(0));
這個代碼循環調用了三次,每次對i累加1。每次函數foo調用的時候,都會有一個創建新的執行上下文。一旦上下文完成了執行,就會推出棧,將控制流返回給它下面的執行上下文,這樣一直到全局上下文。
關于執行棧,有5點需要記?。?/p>
- 單線程
- 同步執行
- 一個全局上下文
- 無數的函數上下文
- 每次函數調用都會床架一個新的執行上下文,即使是調用自身
三:執行上下文詳解
我們已經知道每當一個函數調用發生,都會創建一個新的執行上下文。但是在JS解釋器內部,每次調用一個執行上下文都分為兩個步驟
- 創建階段[在函數被調用,但還未執行任何代碼之前]
- 激活/代碼執行階段:
- 分配變量,以及到函數的引用,然后解析/執行代碼
一個執行上下文從概念上可以視為一個包含三個property的Object
executionContextObj = {
'scopeChain': { /* variableObject + all parent execution context's variableObject */ },
'variableObject': { /* function arguments / parameters, inner variable and function declarations */ },
'this': {}
}
四: Activation / Variable Object [AO/VO]
當調用函數的時候,就會創建executionContextObj
對象,此時真正的函數邏輯還未執行。這就是第一階段---創建階段。在這里,解釋器會掃描函數,根據獲取到的參數/傳參和內部函數聲明/內部變量聲明,來創建executionContextObj
對象。掃描的結果存放在executionContextObj
對象的variableObject
屬性中。
下面是解釋器解析代碼的流程概述:
- 找到被調用函數的代碼內容
- 在執行
function
代碼前,先創建執行上下文execution context
- 進入創建階段
- 初始化
作用域鏈
. - 創建
variable object
:- 創建
arguments object
;檢查上下文獲取入參,初始化形參名稱和數值,并創建一個引用拷貝 - 掃描上下文獲取內部函數聲明:
- 對發現的每一個內部函數,都在
variable object
中創建一個和函數名一樣的property,該property作為一個引用指針指向函數代碼在內存中的地址 - 如果在
variable object
中已經存在相同名稱的property,那么相應的property會被重寫
- 對發現的每一個內部函數,都在
- 掃描上下文獲取內部變量聲明:
- 對發現的每一個內部變量聲明,都在
variable object
中創建一個和變量名一樣的property,并且將其初始化為undefined
- 如果在
variable object
中已經存在相同變量名稱的property,那么就跳過,不做任何動作,繼續掃描
- 對發現的每一個內部變量聲明,都在
- 創建
- 決定在上下文中
"this"
的值
- 初始化
- 激活/代碼執行階段:
- 執行上下文中的函數代碼,逐行運行JS代碼,并給變量賦值
讓我們看個例子
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, // 檢查上下文,發現內部聲明變量a,初始化為undefined
b: undefined // 檢查上下文,發現內部聲明變量b,初始化為undefined,此時并不賦值,右側的函數作為賦值語句,在代碼未執行前,并不存在
},
this: { ... }
}
參見代碼中的備注,在創建階段除了形參參數進行了定義和賦值外,其他只定義了property的名稱,并沒有賦值。一旦創建階段完成,執行流程就進入到函數內部進入激活/代碼執行階段。在執行完后的上下文大致如下:
fooExecutionContext = {
scopeChain: { ... },
variableObject: {
arguments: {
0: 22,
length: 1
},
i: 22,
c: pointer to function c()
a: 'hello',
b: pointer to function privateB()
},
this: { ... }
}
五:關于提升(Hoisting)
網上有很多資源會提到JS特有的變量提升(Hoisting),其中會解釋說JS會將變量和函數聲明提升到函數作用域的頂部。但是,并沒有人詳細解釋為什么會出現這種情況。在掌握了關于解釋器如何創建上下文的知識后,這就非常容易解釋了??聪旅娴拇a:
?(function() {
console.log(typeof foo); // function pointer
console.log(typeof bar); // undefined
var foo = 'hello',
bar = function() {
return 'world';
};
function foo() {
return 'hello';
}
console.log(typeof foo); // string
}());?
我們現在可以回答的問題是:
-
為什么我們可以在聲明foo之前就訪問它?
- 如果我們遵循
creation stage
,我們知道變量在activation / code execution stage
之前就創建了。所以當功能流程開始執行時,在上下文中,foo
已經做了定義。
- 如果我們遵循
-
foo是聲明了兩次,為什么顯示foo的是
function
,不undefined
還是string
?- 即使
foo
聲明了兩次,我們也知道在creation stage
階段,在上下文中,函數是在變量之前創建的,并且如果上下文中一個變量名稱的屬性名已經存在,我們就會忽略掉這個變量聲明。 - 因此,
function foo()
首先在上下文中創建一個名為foo
的引用property,當解釋器到達時var foo
時,我們看到屬性名稱foo
已經存在,所以代碼什么都不做并繼續執行。
- 即使
-
為什么bar是
undefined
?-
bar
實際上是一個具有函數賦值的變量,我們知道變量是在creation stage
階段創建的,但它們是用值會被初始化為undefined
。
-
-
為什么最后foo是
string
?-
foo
在創造階段按照規則被賦予了function的類型,但在執行階段,隨著var foo = 'hello'
的執行,將其變為了String類型,下面的函數聲明在創造階段已經執行,因此跳過后,foo還是String類型
-
六:總結
希望到現在您已經很好地掌握了JavaScript中解釋器是如何處理您的代碼的。理解執行上下文和??梢宰屇私鉃槭裁创a運行的結果和你最初預期的不同的原因。
進一步閱讀: