一、執(zhí)行上下文概念
JavaScript代碼的執(zhí)行過程分為兩個階段:
- 代碼編譯階段:由編譯器完成,將代碼翻譯成可執(zhí)行代碼
- 代碼執(zhí)行階段:由引擎完成,主要任務(wù)是執(zhí)行可執(zhí)行代碼
其中可執(zhí)行代碼分為三種:全局代碼、函數(shù)代碼、eval代碼
有關(guān)JavaScript代碼的執(zhí)行過程可查看《【你不知道的JavaScript】(一)作用域與詞法作用域》一文。
簡單來說,當在代碼執(zhí)行階段執(zhí)行到一個函數(shù)的時候,就會進行準備工作,這里的“準備工作”,就叫做"執(zhí)行上下文(
EC)",也叫執(zhí)行上下文環(huán)境,也叫執(zhí)行環(huán)境。
當JavaScript代碼執(zhí)行時,會進入不同的執(zhí)行上下文,而每個執(zhí)行上下文的組成,基本如下:

二、執(zhí)行上下文生命周期
當調(diào)用一個函數(shù)時(激活),一個新的執(zhí)行上下文就會被創(chuàng)建。而一個執(zhí)行上下文的生命周期可以分為兩個階段:
- 創(chuàng)建階段:在這個階段中,執(zhí)行上下文會分別創(chuàng)建變量對象,建立作用域鏈,以及確定this的指向。
- 執(zhí)行階段:創(chuàng)建完成之后,就會開始執(zhí)行代碼,這個時候,會完成變量賦值,函數(shù)引用,以及執(zhí)行其他代碼。

詳細了解執(zhí)行上下文極為重要,因為其中涉及到了變量對象,作用域鏈,this等極為重要的概念,它關(guān)系到我們能不能真正理解JavaScript,下面我們分別了解幾個概念。
(一)變量對象
1. 變量對象的創(chuàng)建過程
(1) 建立arguments對象。檢查當前上下文中的參數(shù),建立該對象下的屬性與屬性值。
(2) 檢查當前上下文的函數(shù)聲明,也就是使用function關(guān)鍵字聲明的函數(shù)。在變量對象中以函數(shù)名建立一個屬性,屬性值為指向該函數(shù)所在內(nèi)存地址的引用。如果函數(shù)名的屬性已經(jīng)存在,那么該屬性將會被新的引用所覆蓋。
(3) 檢查當前上下文中的變量聲明,每找到一個變量聲明,就在變量對象中以變量名建立一個屬性,屬性值為undefined。如果該變量名的屬性已經(jīng)存在,為了防止同名的函數(shù)被修改為undefined,則會直接跳過,原屬性值不會被修改。

function foo() { console.log('function foo') }
var foo = 20;
console.log(foo); // 20
// ↑以上代碼中,變量聲明的 foo 遇到函數(shù)聲明的 foo 會跳過,
// 可是為什么最后 foo 的輸出結(jié)果仍然是被覆蓋了呢?
// 那是因為三條規(guī)則僅僅適用于變量對象的創(chuàng)建過程,也就是執(zhí)行上下文的創(chuàng)建過程。
// 而 foo=20 是在執(zhí)行上下文的執(zhí)行過程中運行的,輸出結(jié)果自然會是20。
再來看另外一個例子:
console.log(foo); // ? foo() { console.log('function foo') }
function foo() { console.log('function foo') }
var foo = 20;
// 上栗的執(zhí)行順序為
// 首先將所有函數(shù)聲明放入變量對象中
function foo() { console.log('function foo') }
// 其次將所有變量聲明放入變量對象中,
// 但是因為foo已經(jīng)存在同名函數(shù),因此此時會跳過undefined的賦值
// var foo = undefined;
// 然后開始執(zhí)行階段代碼的執(zhí)行
console.log(foo); // function foo
foo = 20;
2. 變量對象與活動對象
變量對象與活動對象其實都是同一個對象,只是處于執(zhí)行上下文的不同生命周期。不過只有處于函數(shù)調(diào)用棧棧頂?shù)膱?zhí)行上下文中的變量對象,才會變成活動對象。
function test() {
console.log(a);
console.log(foo());
var a = 1;
function foo() {
return 2;
}
}
test();
↑以上代碼中,全局作用域中運行test()時,test()的執(zhí)行上下文開始創(chuàng)建。為了便于理解,我們用如下的形式來表示:
// 創(chuàng)建過程
testEC = {
// VO 為 Variable Object的縮寫,即變量對象
VO: {
//注:在瀏覽器的展示中,函數(shù)的參數(shù)可能并不是放在arguments對象中,
//這里為了方便理解,我做了這樣的處理
arguments: {...},
foo: <foo reference>, // 表示 foo 的地址引用
a: undefined,
this: Window
},
scopeChain: {}
}
未進入執(zhí)行階段之前,變量對象中的屬性都不能訪問!但是進入執(zhí)行階段之后,變量對象轉(zhuǎn)變?yōu)榱嘶顒訉ο?/strong>,里面的屬性都能被訪問了,然后開始進行執(zhí)行階段的操作。
// 執(zhí)行階段
VO -> AO // Active Object
AO = {
arguments: {...},
foo: <foo reference>,
a: 1,
this: Window
}
因此,上面例子的執(zhí)行順序如下:
function test() {
function foo() {
return 2;
}
var a;
console.log(a); // undefined
console.log(foo()); // 2
a = 1;
}
test();
3. 全局上下文的變量對象
全局上下文有一個特殊的地方,它的變量對象,就是window對象。而這個特殊,在this指向上也同樣適用,this也是指向window。
除此之外,全局上下文的生命周期,與程序的生命周期一致,只要程序運行不結(jié)束,比如關(guān)掉瀏覽器窗口,全局上下文就會一直存在。其他所有的上下文環(huán)境,都能直接訪問全局上下文的屬性。
(二)作用域鏈
作用域鏈本質(zhì)上是一個指向當前環(huán)境與上層環(huán)境的一系列變量對象的指針列表(它只引用但不實際包含變量對象),作用域鏈保證了當前執(zhí)行環(huán)境對符合訪問權(quán)限的變量和函數(shù)的有序訪問。
var a = 1;
function out() {
var b = 2;
function inner() {
var c = 3;
console.log(a+b+c);
}
inner();
}
out();
首先,代碼開始運行時就創(chuàng)建了全局上下文環(huán)境,接著運行到out()時創(chuàng)建 out函數(shù)的執(zhí)行上下文,最后運行到inner()時創(chuàng)建 inner函數(shù)的執(zhí)行上下文,我們設(shè)定他們的變量對象分別為VO(global),VO(out), VO(inner)。
我們可以直接用一個數(shù)組來表示作用域鏈,數(shù)組的第一項scopeChain[0]為作用域鏈的最前端,而數(shù)組的最后一項,為作用域鏈的最末端,所有的最末端都為全局變量對象。
- 全局的作用域鏈:由于它只含全局作用域,沒有上級,因此它的作用域鏈只指向本身的全局變量對象。查找標識符時只能從本身的全局變量對象中查找。
// 全局上下文環(huán)境
globalEC = {
VO: {
out: <out reference>, // 表示 out 的地址引用
a: undefined
},
scopeChain: [VO(global)], // 作用域鏈
}
-
函數(shù)
out的作用域鏈:可以引用函數(shù)out本身的變量對象以及全局的變量對象。查找標識符時,先在函數(shù)out變量對象中尋找,找不到的話再去上一級全局變量對象查找。
// out 函數(shù)的執(zhí)行上下文
outEC = {
VO: {
arguments: {...},
inner: <inner reference>, // 表示 inner 的地址引用
b: undefined
},
scopeChain: [VO(out), VO(global)], // 作用域鏈
}

-
函數(shù)
inner的作用域鏈:可以引用函數(shù)inner本身的變量對象和上一級out函數(shù)的變量對象以及全局的變量對象。查找標識符時依次從inner,out,全局變量對象中查找。
innerEC = {
VO: {
arguments: {...},
c: undefined,
}, // 變量對象
scopeChain: [VO(inner), VO(out), VO(global)], // 作用域鏈
}

(三)this指向
有關(guān)this的指向的詳情,可查看《【你不知道的JavaScript】(四)this的全面解析》。
三、執(zhí)行上下文棧
執(zhí)行上下文可以理解為當前代碼的執(zhí)行環(huán)境,JavaScript中的運行環(huán)境大概包括三種情況:
-
全局環(huán)境:
JavaScript代碼運行起來會首先進入該環(huán)境 - 函數(shù)環(huán)境:當函數(shù)被調(diào)用執(zhí)行時,會進入當前函數(shù)中執(zhí)行代碼
eval
在代碼開始執(zhí)行時,首先會產(chǎn)生一個全局執(zhí)行上下文環(huán)境,調(diào)用函數(shù)時,會產(chǎn)生函數(shù)執(zhí)行上下文環(huán)境,函數(shù)調(diào)用完成后,它的執(zhí)行上下文環(huán)境以及其中的數(shù)據(jù)都會被銷毀,重新回到全局執(zhí)行環(huán)境,網(wǎng)頁關(guān)閉后全局執(zhí)行環(huán)境也會銷毀。其實這是一個壓棧出棧的過程,全局上下文環(huán)境永遠在棧底,而當前正在執(zhí)行的函數(shù)上下文在棧頂。
var a = 1; //1.進入全局上下文環(huán)境
function out() {
var b = 2;
function inner() {
var c = 3;
console.log(a+b+c);
}
inner(); //3.進入inner函數(shù)上下文環(huán)境
}
out(); //2.進入out函數(shù)上下文環(huán)境
↑以上代碼的執(zhí)行會經(jīng)歷以下過程:
- 當代碼開始執(zhí)行時就創(chuàng)建全局執(zhí)行上下文環(huán)境,全局上下文入棧。
- 全局上下文入棧后,其中的代碼開始執(zhí)行,進行賦值、函數(shù)調(diào)用等操作,執(zhí)行到
out()時,激活函數(shù)out創(chuàng)建自己的執(zhí)行上下文環(huán)境,out函數(shù)上下文入棧。 -
out函數(shù)上下文入棧后,其中的代碼開始執(zhí)行,進行賦值、函數(shù)調(diào)用等操作,執(zhí)行到inner()時,激活函數(shù)inner創(chuàng)建自己的執(zhí)行上下文環(huán)境,inner函數(shù)上下文入棧。 -
inner函數(shù)上下文入棧后,其中的代碼開始執(zhí)行,進行賦值、函數(shù)調(diào)用、打印等操作,由于里面沒有可以生成其他執(zhí)行上下文的需要,所有代碼執(zhí)行完畢后,inner函數(shù)上下文出棧。 -
inner函數(shù)上下文出棧,又回到了out函數(shù)執(zhí)行上下文環(huán)境,接著執(zhí)行out函數(shù)中后面剩下的代碼,由于后面沒有可以生成其他執(zhí)行上下文的需要,所有代碼執(zhí)行完畢后,out函數(shù)上下文出棧。 -
out函數(shù)上下文出棧后,又回到了全局執(zhí)行上下文環(huán)境,直到瀏覽器窗口關(guān)閉,全局上下文出棧。

我們可以得到一些結(jié)論:
- 全局上下文在代碼開始執(zhí)行時就創(chuàng)建,只有唯一的一個,永遠在棧底,瀏覽器窗口關(guān)閉時出棧。
- 函數(shù)被調(diào)用的時候創(chuàng)建上下文環(huán)境。
- 只有棧頂?shù)纳舷挛奶幱诨顒訝顟B(tài),執(zhí)行其中的代碼。