本文翻譯之 http://davidshariff.com/blog/what-is-the-execution-context-in-javascript/
在這篇文章中,我將深入探討JavaScript中一個(gè)最基本的部分,即Execution Context。 在本文結(jié)束時(shí),您應(yīng)該更清楚地知道解釋器是怎么工作的,為什么某些函數(shù)/變量在聲明之前就可以使用以及它們的值是如何確定的。
一:什么是執(zhí)行上下文?
當(dāng)JavaScript代碼運(yùn)行的時(shí)候,確定它運(yùn)行所在的環(huán)境是非常重要的。運(yùn)行環(huán)境由下面三種不同的代碼類型確定
- 全局代碼(Global Code):代碼首次執(zhí)行時(shí)候的默認(rèn)環(huán)境
- 函數(shù)代碼(Function Code):每當(dāng)執(zhí)行流程進(jìn)入到一個(gè)函數(shù)體內(nèi)部的時(shí)候
- Eval代碼(Eval Code):當(dāng)eval函數(shù)內(nèi)部的文本執(zhí)行的時(shí)候
您可以在網(wǎng)上找到大量關(guān)于scope的參考資料。為了更易于理解,我們將execution context簡單視為運(yùn)行當(dāng)前代碼的environment/scope。好了,話不多說,先讓我們看個(gè)例子,其中包含了global context和function/local context 代碼。

在上圖中,我們有1個(gè)全局上下文(Global Context),使用紫色邊框表示;有3個(gè)不同的函數(shù)上下文(Function Context)由綠色,藍(lán)色,和橙色邊框表示。注意!全局上下文有且只有一個(gè),程序中其他任意的上下文都可以訪問全局上下文。
你可以擁有任意數(shù)量的函數(shù)上下文。每一次函數(shù)調(diào)用都會創(chuàng)建一個(gè)新的上下文,它會創(chuàng)建一個(gè)私有域,函數(shù)內(nèi)部做出的所有聲明都會放在這個(gè)私有域中,并且這些聲明在當(dāng)前函數(shù)作用域外無法直接訪問。在上面的例子中,一個(gè)函數(shù)可以訪問它所在的上下文尾部的變量,但是一個(gè)外部的上下文無法訪問內(nèi)部函數(shù)內(nèi)部聲明的變量/函數(shù)。為什么會發(fā)生這樣的情況?代碼究竟是如何被解析的呢?
二:執(zhí)行上下文棧
瀏覽器中的JS解釋器是單線程的。也就是說在瀏覽器中同一時(shí)間只能做一個(gè)事情,其他的action和event都會被排隊(duì)放入到執(zhí)行棧中(Execution Stack)。下圖表示了一個(gè)單線程棧的抽象視圖

如我們所知,當(dāng)一個(gè)瀏覽器第一次load你的代碼的時(shí)候,首先它會進(jìn)入到一個(gè)全局執(zhí)行上下文中。如果在你的全局代碼中,你調(diào)用了一個(gè)函數(shù),那么程序的執(zhí)行流程會進(jìn)入到被調(diào)用的函數(shù)中,并創(chuàng)建一個(gè)新的執(zhí)行上下文,并將這個(gè)上下文推入到執(zhí)行棧頂。
如果在當(dāng)前的函數(shù)中,你由調(diào)用了一個(gè)函數(shù),那么也會執(zhí)行同樣的操作。執(zhí)行流程計(jì)入到剛被調(diào)用的函數(shù)內(nèi)部,重新創(chuàng)建一個(gè)新的執(zhí)行上下文,并再次推入到執(zhí)行棧頂。瀏覽器會一直執(zhí)行當(dāng)前棧頂?shù)膱?zhí)行上下文,一旦函數(shù)執(zhí)行完畢,該上下文就會被推出執(zhí)行棧。下面的例子展示了一個(gè)遞歸函數(shù)以及該程序的執(zhí)行棧:
(function foo(i) {
if (i === 3) {
return;
}
else {
foo(++i);
}
}(0));

這個(gè)代碼循環(huán)調(diào)用了三次,每次對i累加1。每次函數(shù)foo調(diào)用的時(shí)候,都會有一個(gè)創(chuàng)建新的執(zhí)行上下文。一旦上下文完成了執(zhí)行,就會推出棧,將控制流返回給它下面的執(zhí)行上下文,這樣一直到全局上下文。
關(guān)于執(zhí)行棧,有5點(diǎn)需要記?。?/p>
- 單線程
- 同步執(zhí)行
- 一個(gè)全局上下文
- 無數(shù)的函數(shù)上下文
- 每次函數(shù)調(diào)用都會床架一個(gè)新的執(zhí)行上下文,即使是調(diào)用自身
三:執(zhí)行上下文詳解
我們已經(jīng)知道每當(dāng)一個(gè)函數(shù)調(diào)用發(fā)生,都會創(chuàng)建一個(gè)新的執(zhí)行上下文。但是在JS解釋器內(nèi)部,每次調(diào)用一個(gè)執(zhí)行上下文都分為兩個(gè)步驟
- 創(chuàng)建階段[在函數(shù)被調(diào)用,但還未執(zhí)行任何代碼之前]
- 激活/代碼執(zhí)行階段:
- 分配變量,以及到函數(shù)的引用,然后解析/執(zhí)行代碼
一個(gè)執(zhí)行上下文從概念上可以視為一個(gè)包含三個(gè)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]
當(dāng)調(diào)用函數(shù)的時(shí)候,就會創(chuàng)建executionContextObj對象,此時(shí)真正的函數(shù)邏輯還未執(zhí)行。這就是第一階段---創(chuàng)建階段。在這里,解釋器會掃描函數(shù),根據(jù)獲取到的參數(shù)/傳參和內(nèi)部函數(shù)聲明/內(nèi)部變量聲明,來創(chuàng)建executionContextObj對象。掃描的結(jié)果存放在executionContextObj對象的variableObject屬性中。
下面是解釋器解析代碼的流程概述:
- 找到被調(diào)用函數(shù)的代碼內(nèi)容
- 在執(zhí)行
function代碼前,先創(chuàng)建執(zhí)行上下文execution context - 進(jìn)入創(chuàng)建階段
- 初始化
作用域鏈. - 創(chuàng)建
variable object:- 創(chuàng)建
arguments object;檢查上下文獲取入?yún)?,初始化形參名稱和數(shù)值,并創(chuàng)建一個(gè)引用拷貝 - 掃描上下文獲取內(nèi)部函數(shù)聲明:
- 對發(fā)現(xiàn)的每一個(gè)內(nèi)部函數(shù),都在
variable object中創(chuàng)建一個(gè)和函數(shù)名一樣的property,該property作為一個(gè)引用指針指向函數(shù)代碼在內(nèi)存中的地址 - 如果在
variable object中已經(jīng)存在相同名稱的property,那么相應(yīng)的property會被重寫
- 對發(fā)現(xiàn)的每一個(gè)內(nèi)部函數(shù),都在
- 掃描上下文獲取內(nèi)部變量聲明:
- 對發(fā)現(xiàn)的每一個(gè)內(nèi)部變量聲明,都在
variable object中創(chuàng)建一個(gè)和變量名一樣的property,并且將其初始化為undefined - 如果在
variable object中已經(jīng)存在相同變量名稱的property,那么就跳過,不做任何動作,繼續(xù)掃描
- 對發(fā)現(xiàn)的每一個(gè)內(nèi)部變量聲明,都在
- 創(chuàng)建
- 決定在上下文中
"this"的值
- 初始化
- 激活/代碼執(zhí)行階段:
- 執(zhí)行上下文中的函數(shù)代碼,逐行運(yùn)行JS代碼,并給變量賦值
讓我們看個(gè)例子
function foo(i) {
var a = 'hello';
var b = function privateB() {
};
function c() {
}
}
foo(22);
當(dāng)剛調(diào)用foo(22)函數(shù)的時(shí)候,創(chuàng)建階段的上下文大致是下面的樣子:
fooExecutionContext = {
scopeChain: { ... },
variableObject: {
arguments: { // 創(chuàng)建了參數(shù)對象
0: 22,
length: 1
},
i: 22, // 檢查上下文,創(chuàng)建形參名稱,賦值/或創(chuàng)建引用拷貝
c: pointer to function c() // 檢查上下文,發(fā)現(xiàn)內(nèi)部函數(shù)聲明,創(chuàng)建引用指向函數(shù)體
a: undefined, // 檢查上下文,發(fā)現(xiàn)內(nèi)部聲明變量a,初始化為undefined
b: undefined // 檢查上下文,發(fā)現(xiàn)內(nèi)部聲明變量b,初始化為undefined,此時(shí)并不賦值,右側(cè)的函數(shù)作為賦值語句,在代碼未執(zhí)行前,并不存在
},
this: { ... }
}
參見代碼中的備注,在創(chuàng)建階段除了形參參數(shù)進(jìn)行了定義和賦值外,其他只定義了property的名稱,并沒有賦值。一旦創(chuàng)建階段完成,執(zhí)行流程就進(jìn)入到函數(shù)內(nèi)部進(jìn)入激活/代碼執(zhí)行階段。在執(zhí)行完后的上下文大致如下:
fooExecutionContext = {
scopeChain: { ... },
variableObject: {
arguments: {
0: 22,
length: 1
},
i: 22,
c: pointer to function c()
a: 'hello',
b: pointer to function privateB()
},
this: { ... }
}
五:關(guān)于提升(Hoisting)
網(wǎng)上有很多資源會提到JS特有的變量提升(Hoisting),其中會解釋說JS會將變量和函數(shù)聲明提升到函數(shù)作用域的頂部。但是,并沒有人詳細(xì)解釋為什么會出現(xiàn)這種情況。在掌握了關(guān)于解釋器如何創(chuàng)建上下文的知識后,這就非常容易解釋了??聪旅娴拇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
}());?
我們現(xiàn)在可以回答的問題是:
-
為什么我們可以在聲明foo之前就訪問它?
- 如果我們遵循
creation stage,我們知道變量在activation / code execution stage之前就創(chuàng)建了。所以當(dāng)功能流程開始執(zhí)行時(shí),在上下文中,foo已經(jīng)做了定義。
- 如果我們遵循
-
foo是聲明了兩次,為什么顯示foo的是
function,不undefined還是string?- 即使
foo聲明了兩次,我們也知道在creation stage階段,在上下文中,函數(shù)是在變量之前創(chuàng)建的,并且如果上下文中一個(gè)變量名稱的屬性名已經(jīng)存在,我們就會忽略掉這個(gè)變量聲明。 - 因此,
function foo()首先在上下文中創(chuàng)建一個(gè)名為foo的引用property,當(dāng)解釋器到達(dá)時(shí)var foo時(shí),我們看到屬性名稱foo已經(jīng)存在,所以代碼什么都不做并繼續(xù)執(zhí)行。
- 即使
-
為什么bar是
undefined?-
bar實(shí)際上是一個(gè)具有函數(shù)賦值的變量,我們知道變量是在creation stage階段創(chuàng)建的,但它們是用值會被初始化為undefined。
-
-
為什么最后foo是
string?-
foo在創(chuàng)造階段按照規(guī)則被賦予了function的類型,但在執(zhí)行階段,隨著var foo = 'hello'的執(zhí)行,將其變?yōu)榱薙tring類型,下面的函數(shù)聲明在創(chuàng)造階段已經(jīng)執(zhí)行,因此跳過后,foo還是String類型
-
六:總結(jié)
希望到現(xiàn)在您已經(jīng)很好地掌握了JavaScript中解釋器是如何處理您的代碼的。理解執(zhí)行上下文和??梢宰屇私鉃槭裁创a運(yùn)行的結(jié)果和你最初預(yù)期的不同的原因。
進(jìn)一步閱讀: