一案怯、執(zhí)行上下文概念
JavaScript
代碼的執(zhí)行過(guò)程分為兩個(gè)階段:
- 代碼編譯階段:由編譯器完成卜录,將代碼翻譯成可執(zhí)行代碼
- 代碼執(zhí)行階段:由引擎完成少办,主要任務(wù)是執(zhí)行可執(zhí)行代碼
其中可執(zhí)行代碼分為三種:全局代碼扼鞋、函數(shù)代碼获讳、eval
代碼
有關(guān)JavaScript
代碼的執(zhí)行過(guò)程可查看《【你不知道的JavaScript】(一)作用域與詞法作用域》一文阴颖。
簡(jiǎn)單來(lái)說(shuō),當(dāng)在代碼執(zhí)行階段執(zhí)行到一個(gè)函數(shù)的時(shí)候丐膝,就會(huì)進(jìn)行準(zhǔn)備工作量愧,這里的“準(zhǔn)備工作”,就叫做"執(zhí)行上下文(
EC
)"帅矗,也叫執(zhí)行上下文環(huán)境偎肃,也叫執(zhí)行環(huán)境。
當(dāng)JavaScript
代碼執(zhí)行時(shí)浑此,會(huì)進(jìn)入不同的執(zhí)行上下文累颂,而每個(gè)執(zhí)行上下文的組成,基本如下:
二、執(zhí)行上下文生命周期
當(dāng)調(diào)用一個(gè)函數(shù)時(shí)(激活)紊馏,一個(gè)新的執(zhí)行上下文就會(huì)被創(chuàng)建料饥。而一個(gè)執(zhí)行上下文的生命周期可以分為兩個(gè)階段:
- 創(chuàng)建階段:在這個(gè)階段中,執(zhí)行上下文會(huì)分別創(chuàng)建變量對(duì)象朱监,建立作用域鏈岸啡,以及確定this的指向。
- 執(zhí)行階段:創(chuàng)建完成之后赫编,就會(huì)開(kāi)始執(zhí)行代碼巡蘸,這個(gè)時(shí)候,會(huì)完成變量賦值沛慢,函數(shù)引用赡若,以及執(zhí)行其他代碼。
詳細(xì)了解執(zhí)行上下文極為重要团甲,因?yàn)槠渲猩婕暗搅俗兞繉?duì)象逾冬,作用域鏈,this
等極為重要的概念躺苦,它關(guān)系到我們能不能真正理解JavaScript
身腻,下面我們分別了解幾個(gè)概念。
(一)變量對(duì)象
1. 變量對(duì)象的創(chuàng)建過(guò)程
(1) 建立arguments
對(duì)象匹厘。檢查當(dāng)前上下文中的參數(shù)嘀趟,建立該對(duì)象下的屬性與屬性值。
(2) 檢查當(dāng)前上下文的函數(shù)聲明愈诚,也就是使用function
關(guān)鍵字聲明的函數(shù)她按。在變量對(duì)象中以函數(shù)名建立一個(gè)屬性,屬性值為指向該函數(shù)所在內(nèi)存地址的引用炕柔。如果函數(shù)名的屬性已經(jīng)存在酌泰,那么該屬性將會(huì)被新的引用所覆蓋。
(3) 檢查當(dāng)前上下文中的變量聲明匕累,每找到一個(gè)變量聲明陵刹,就在變量對(duì)象中以變量名建立一個(gè)屬性,屬性值為undefined
欢嘿。如果該變量名的屬性已經(jīng)存在衰琐,為了防止同名的函數(shù)被修改為undefined
,則會(huì)直接跳過(guò)炼蹦,原屬性值不會(huì)被修改羡宙。
function foo() { console.log('function foo') }
var foo = 20;
console.log(foo); // 20
// ↑以上代碼中,變量聲明的 foo 遇到函數(shù)聲明的 foo 會(huì)跳過(guò)框弛,
// 可是為什么最后 foo 的輸出結(jié)果仍然是被覆蓋了呢辛辨?
// 那是因?yàn)槿龡l規(guī)則僅僅適用于變量對(duì)象的創(chuàng)建過(guò)程,也就是執(zhí)行上下文的創(chuàng)建過(guò)程。
// 而 foo=20 是在執(zhí)行上下文的執(zhí)行過(guò)程中運(yùn)行的斗搞,輸出結(jié)果自然會(huì)是20指攒。
再來(lái)看另外一個(gè)例子:
console.log(foo); // ? foo() { console.log('function foo') }
function foo() { console.log('function foo') }
var foo = 20;
// 上栗的執(zhí)行順序?yàn)?// 首先將所有函數(shù)聲明放入變量對(duì)象中
function foo() { console.log('function foo') }
// 其次將所有變量聲明放入變量對(duì)象中,
// 但是因?yàn)閒oo已經(jīng)存在同名函數(shù)僻焚,因此此時(shí)會(huì)跳過(guò)undefined的賦值
// var foo = undefined;
// 然后開(kāi)始執(zhí)行階段代碼的執(zhí)行
console.log(foo); // function foo
foo = 20;
2. 變量對(duì)象與活動(dòng)對(duì)象
變量對(duì)象與活動(dòng)對(duì)象其實(shí)都是同一個(gè)對(duì)象允悦,只是處于執(zhí)行上下文的不同生命周期。不過(guò)只有處于函數(shù)調(diào)用棧棧頂?shù)膱?zhí)行上下文中的變量對(duì)象虑啤,才會(huì)變成活動(dòng)對(duì)象隙弛。
function test() {
console.log(a);
console.log(foo());
var a = 1;
function foo() {
return 2;
}
}
test();
↑以上代碼中,全局作用域中運(yùn)行test()
時(shí)狞山,test()
的執(zhí)行上下文開(kāi)始創(chuàng)建全闷。為了便于理解,我們用如下的形式來(lái)表示:
// 創(chuàng)建過(guò)程
testEC = {
// VO 為 Variable Object的縮寫(xiě)萍启,即變量對(duì)象
VO: {
//注:在瀏覽器的展示中总珠,函數(shù)的參數(shù)可能并不是放在arguments對(duì)象中,
//這里為了方便理解勘纯,我做了這樣的處理
arguments: {...},
foo: <foo reference>, // 表示 foo 的地址引用
a: undefined,
this: Window
},
scopeChain: {}
}
未進(jìn)入執(zhí)行階段之前局服,變量對(duì)象中的屬性都不能訪問(wèn)!但是進(jìn)入執(zhí)行階段之后驳遵,變量對(duì)象轉(zhuǎn)變?yōu)榱嘶顒?dòng)對(duì)象淫奔,里面的屬性都能被訪問(wèn)了,然后開(kāi)始進(jìn)行執(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. 全局上下文的變量對(duì)象
全局上下文有一個(gè)特殊的地方,它的變量對(duì)象竞穷,就是window
對(duì)象媒惕。而這個(gè)特殊,在this
指向上也同樣適用来庭,this
也是指向window
。
除此之外穿挨,全局上下文的生命周期月弛,與程序的生命周期一致,只要程序運(yùn)行不結(jié)束科盛,比如關(guān)掉瀏覽器窗口帽衙,全局上下文就會(huì)一直存在。其他所有的上下文環(huán)境贞绵,都能直接訪問(wèn)全局上下文的屬性厉萝。
(二)作用域鏈
作用域鏈本質(zhì)上是一個(gè)指向當(dāng)前環(huán)境與上層環(huán)境的一系列變量對(duì)象的指針列表(它只引用但不實(shí)際包含變量對(duì)象),作用域鏈保證了當(dāng)前執(zhí)行環(huán)境對(duì)符合訪問(wèn)權(quán)限的變量和函數(shù)的有序訪問(wèn)。
var a = 1;
function out() {
var b = 2;
function inner() {
var c = 3;
console.log(a+b+c);
}
inner();
}
out();
首先谴垫,代碼開(kāi)始運(yùn)行時(shí)就創(chuàng)建了全局上下文環(huán)境章母,接著運(yùn)行到out()
時(shí)創(chuàng)建 out
函數(shù)的執(zhí)行上下文,最后運(yùn)行到inner()
時(shí)創(chuàng)建 inner
函數(shù)的執(zhí)行上下文翩剪,我們?cè)O(shè)定他們的變量對(duì)象分別為VO(global)
乳怎,VO(out)
, VO(inner)
。
我們可以直接用一個(gè)數(shù)組來(lái)表示作用域鏈前弯,數(shù)組的第一項(xiàng)scopeChain[0]
為作用域鏈的最前端蚪缀,而數(shù)組的最后一項(xiàng),為作用域鏈的最末端恕出,所有的最末端都為全局變量對(duì)象询枚。
- 全局的作用域鏈:由于它只含全局作用域,沒(méi)有上級(jí)浙巫,因此它的作用域鏈只指向本身的全局變量對(duì)象金蜀。查找標(biāo)識(shí)符時(shí)只能從本身的全局變量對(duì)象中查找。
// 全局上下文環(huán)境
globalEC = {
VO: {
out: <out reference>, // 表示 out 的地址引用
a: undefined
},
scopeChain: [VO(global)], // 作用域鏈
}
-
函數(shù)
out
的作用域鏈:可以引用函數(shù)out
本身的變量對(duì)象以及全局的變量對(duì)象狈醉。查找標(biāo)識(shí)符時(shí)廉油,先在函數(shù)out
變量對(duì)象中尋找,找不到的話再去上一級(jí)全局變量對(duì)象查找苗傅。
// out 函數(shù)的執(zhí)行上下文
outEC = {
VO: {
arguments: {...},
inner: <inner reference>, // 表示 inner 的地址引用
b: undefined
},
scopeChain: [VO(out), VO(global)], // 作用域鏈
}
-
函數(shù)
inner
的作用域鏈:可以引用函數(shù)inner
本身的變量對(duì)象和上一級(jí)out
函數(shù)的變量對(duì)象以及全局的變量對(duì)象抒线。查找標(biāo)識(shí)符時(shí)依次從inner
,out
渣慕,全局變量對(duì)象中查找嘶炭。
innerEC = {
VO: {
arguments: {...},
c: undefined,
}, // 變量對(duì)象
scopeChain: [VO(inner), VO(out), VO(global)], // 作用域鏈
}
(三)this
指向
有關(guān)this
的指向的詳情,可查看《【你不知道的JavaScript】(四)this的全面解析》逊桦。
三眨猎、執(zhí)行上下文棧
執(zhí)行上下文可以理解為當(dāng)前代碼的執(zhí)行環(huán)境,JavaScript
中的運(yùn)行環(huán)境大概包括三種情況:
-
全局環(huán)境:
JavaScript
代碼運(yùn)行起來(lái)會(huì)首先進(jìn)入該環(huán)境 - 函數(shù)環(huán)境:當(dāng)函數(shù)被調(diào)用執(zhí)行時(shí)强经,會(huì)進(jìn)入當(dāng)前函數(shù)中執(zhí)行代碼
eval
在代碼開(kāi)始執(zhí)行時(shí)睡陪,首先會(huì)產(chǎn)生一個(gè)全局執(zhí)行上下文環(huán)境,調(diào)用函數(shù)時(shí)匿情,會(huì)產(chǎn)生函數(shù)執(zhí)行上下文環(huán)境兰迫,函數(shù)調(diào)用完成后,它的執(zhí)行上下文環(huán)境以及其中的數(shù)據(jù)都會(huì)被銷毀炬称,重新回到全局執(zhí)行環(huán)境汁果,網(wǎng)頁(yè)關(guān)閉后全局執(zhí)行環(huán)境也會(huì)銷毀。其實(shí)這是一個(gè)壓棧出棧的過(guò)程玲躯,全局上下文環(huán)境永遠(yuǎn)在棧底据德,而當(dāng)前正在執(zhí)行的函數(shù)上下文在棧頂鳄乏。
var a = 1; //1.進(jìn)入全局上下文環(huán)境
function out() {
var b = 2;
function inner() {
var c = 3;
console.log(a+b+c);
}
inner(); //3.進(jìn)入inner函數(shù)上下文環(huán)境
}
out(); //2.進(jìn)入out函數(shù)上下文環(huán)境
↑以上代碼的執(zhí)行會(huì)經(jīng)歷以下過(guò)程:
- 當(dāng)代碼開(kāi)始執(zhí)行時(shí)就創(chuàng)建全局執(zhí)行上下文環(huán)境,全局上下文入棧棘利。
- 全局上下文入棧后橱野,其中的代碼開(kāi)始執(zhí)行,進(jìn)行賦值赡译、函數(shù)調(diào)用等操作仲吏,執(zhí)行到
out()
時(shí),激活函數(shù)out
創(chuàng)建自己的執(zhí)行上下文環(huán)境蝌焚,out
函數(shù)上下文入棧裹唆。 -
out
函數(shù)上下文入棧后,其中的代碼開(kāi)始執(zhí)行只洒,進(jìn)行賦值许帐、函數(shù)調(diào)用等操作,執(zhí)行到inner()
時(shí)毕谴,激活函數(shù)inner
創(chuàng)建自己的執(zhí)行上下文環(huán)境成畦,inner
函數(shù)上下文入棧。 -
inner
函數(shù)上下文入棧后涝开,其中的代碼開(kāi)始執(zhí)行循帐,進(jìn)行賦值、函數(shù)調(diào)用舀武、打印等操作拄养,由于里面沒(méi)有可以生成其他執(zhí)行上下文的需要,所有代碼執(zhí)行完畢后银舱,inner
函數(shù)上下文出棧瘪匿。 -
inner
函數(shù)上下文出棧,又回到了out
函數(shù)執(zhí)行上下文環(huán)境寻馏,接著執(zhí)行out
函數(shù)中后面剩下的代碼棋弥,由于后面沒(méi)有可以生成其他執(zhí)行上下文的需要,所有代碼執(zhí)行完畢后诚欠,out
函數(shù)上下文出棧顽染。 -
out
函數(shù)上下文出棧后,又回到了全局執(zhí)行上下文環(huán)境轰绵,直到瀏覽器窗口關(guān)閉家乘,全局上下文出棧。
我們可以得到一些結(jié)論:
- 全局上下文在代碼開(kāi)始執(zhí)行時(shí)就創(chuàng)建藏澳,只有唯一的一個(gè),永遠(yuǎn)在棧底耀找,瀏覽器窗口關(guān)閉時(shí)出棧翔悠。
- 函數(shù)被調(diào)用的時(shí)候創(chuàng)建上下文環(huán)境业崖。
- 只有棧頂?shù)纳舷挛奶幱诨顒?dòng)狀態(tài),執(zhí)行其中的代碼蓄愁。