一、執(zhí)行上下文
JavaScript中的運(yùn)行環(huán)境包括三種情況:
- 全局環(huán)境(Global Code):JavaScript代碼運(yùn)行起來(lái)會(huì)首先進(jìn)去該環(huán)境
- 函數(shù)環(huán)境(Function Code):當(dāng)函數(shù)被調(diào)用執(zhí)行時(shí),會(huì)進(jìn)入當(dāng)前函數(shù)中執(zhí)行代碼
- Eval Code:使用eval()執(zhí)行代碼
為了表示不同的運(yùn)行環(huán)境,JavaScript中有一個(gè)執(zhí)行上下文(Execution context,EC)的概念。執(zhí)行上下文可以理解為當(dāng)前代碼的執(zhí)行環(huán)境,它會(huì)形成一個(gè)作用域。每次當(dāng)控制器轉(zhuǎn)到可執(zhí)行代碼的時(shí)候,就會(huì)進(jìn)入一個(gè)執(zhí)行上下文。
在一個(gè)JavaScript程序中,必定會(huì)產(chǎn)生多個(gè)執(zhí)行上下文,JavaScript引擎會(huì)以堆棧的方式來(lái)處理它們,這個(gè)堆棧稱(chēng)其為函數(shù)調(diào)用棧(call stack)。棧底永遠(yuǎn)都是全局上下文,而棧頂就是當(dāng)前正在執(zhí)行的上下文。
當(dāng)代碼在執(zhí)行過(guò)程中,遇到以上三種情況,都會(huì)生成一個(gè)執(zhí)行上下文,放入棧中,而處于棧頂?shù)纳舷挛膱?zhí)行完畢之后,就會(huì)自動(dòng)出棧。例:
用ECStack來(lái)處理執(zhí)行上下文組的堆棧。
- 第一步:全局上下文入棧。
全局上下文入棧之后,其中的可執(zhí)行代碼開(kāi)始執(zhí)行,直到遇到了changeColor(),這一句激活函數(shù)changeColor創(chuàng)建它自己的執(zhí)行上下文
- 第二步:changeColor的執(zhí)行上下文入棧。
changeColor的執(zhí)行上下文入棧之后,控制器開(kāi)始執(zhí)行其中的可執(zhí)行代碼,遇到swapColors()之后又激活了一個(gè)執(zhí)行上下文。
- 第三步:swapColors的執(zhí)行上下文入棧。
在swapColors的可執(zhí)行代碼中,再?zèng)]有遇到其他能生成執(zhí)行上下文的情況,一次這段代碼順利執(zhí)行完畢,swapColors的上下文從棧中彈出
-第四步:swapColors的執(zhí)行上下文出棧。
swapColors的執(zhí)行上下文彈出之后,繼續(xù)執(zhí)行changeColor的可執(zhí)行代碼,也沒(méi)有再遇到其他執(zhí)行上下文,順利執(zhí)行完畢之后彈出。這樣,ECStack中就只身下全局上下文了
- 第五步:changeColor的執(zhí)行上下文出棧。
全局上下文在瀏覽器窗口關(guān)閉后出棧
- 注意:函數(shù)中,遇到return能直接終止可執(zhí)行代碼的執(zhí)行,因此會(huì)直接將當(dāng)前上下文彈出棧。
了解以上過(guò)程之后,對(duì)執(zhí)行上下文的總結(jié)的一些結(jié)論:
- 單線(xiàn)程
- 同步執(zhí)行,只有棧頂?shù)纳舷挛奶幱趫?zhí)行中,其他上下文需要等待
- 全局上下文只有唯一的一個(gè),它在瀏覽器關(guān)閉時(shí)出棧
- 函數(shù)的執(zhí)行上下文的個(gè)數(shù)沒(méi)有限制
- 每次某個(gè)函數(shù)被調(diào)用,就會(huì)有個(gè)新的執(zhí)行上下文為其創(chuàng)建,即使是調(diào)用的自身函數(shù),也是如此。
為了鞏固一下執(zhí)行上下文的理解,再來(lái)繪制一個(gè)例子的演變過(guò)程,這是一個(gè)簡(jiǎn)單的閉包例子。
因?yàn)閒1中的函數(shù)f2在f1的可執(zhí)行代碼中,并沒(méi)有被調(diào)用執(zhí)行,因此執(zhí)行f1時(shí),f2不會(huì)創(chuàng)建新的上下文,而直到result執(zhí)行時(shí),才創(chuàng)建了一個(gè)新的。具體演變過(guò)程如下。
當(dāng)調(diào)用一個(gè)函數(shù)時(shí)(激活),一個(gè)新的執(zhí)行上下文就會(huì)被創(chuàng)建。而一個(gè)執(zhí)行上下文的生命周期可以分為兩個(gè)階段。
- 創(chuàng)建階段,在這個(gè)階段中,執(zhí)行上下文分別創(chuàng)建變量對(duì)象,建立作用域鏈,以及確定this的指向。
- 代碼執(zhí)行階段,創(chuàng)建完成之后,就會(huì)開(kāi)始執(zhí)行代碼,這個(gè)時(shí)候,會(huì)完成變量賦值,函數(shù)引用,以及執(zhí)行其他代碼。
從這里可以看出了解執(zhí)行上下文極為重要,其中涉及到變量對(duì)象,作用域鏈,this等概念。
1、變量對(duì)象
變量對(duì)象的創(chuàng)建,依次經(jīng)歷了以下幾個(gè)過(guò)程:
(1)建立arguments對(duì)象。檢查當(dāng)前上下文中的參數(shù),建立該對(duì)象下的屬性與屬性值。
(2)檢查當(dāng)前上下文的函數(shù)聲明,也就是使用function關(guān)鍵字聲明的函數(shù)。在變量對(duì)象中以函數(shù)名建立屬性,屬性值為指向該函數(shù)所在內(nèi)存地址的引用。如果函數(shù)名的屬性已經(jīng)存在,那么該屬性將會(huì)被新的引用所覆蓋。
(3)檢查當(dāng)前上下文中的變量聲明,就在變量對(duì)象中以變量名建立一個(gè)屬性,屬性值為undefined。如果改變量名的屬性已經(jīng)存在,為了防止同名函數(shù)被修改為undefined,則會(huì)直接跳過(guò),原屬性值不會(huì)被修改。
可以用變量對(duì)象的創(chuàng)建過(guò)程解釋變量提升;什么是變量提升呢?看下面的例子:
除了有變量提升還有函數(shù)提升
函數(shù)提升是把整個(gè)函數(shù)都提到前面去,函數(shù)有兩種寫(xiě)法,一種是函數(shù)聲明方式,另外一種是函數(shù)表達(dá)式,只有函數(shù)聲明形式才能被提升。
在上面變量對(duì)象的創(chuàng)建的規(guī)則中我們看出,function聲明會(huì)比var聲明優(yōu)先級(jí)更高一點(diǎn)。為了更好的理解變量對(duì)象,在結(jié)合一些簡(jiǎn)單的例子進(jìn)行探討:
從test()的執(zhí)行上下文開(kāi)始理解。全局作用域中運(yùn)行test()時(shí),test()的執(zhí)行上下文開(kāi)始創(chuàng)建,
創(chuàng)建過(guò)程(創(chuàng)建變量對(duì)象,建立作用域鏈,以及確定this的指向):
testEC={
//變量對(duì)象
VO:{}, //VO 為 Variable Object的縮寫(xiě),即變量對(duì)象
scopeChain:{},
this:{}
}
VO={
arguments:{...}, //注:在瀏覽器的展示中,函數(shù)的參數(shù)可能并不是放在arguments對(duì)象中,這里為了方便理解,做了這樣的處理
foo:<foo reference> // 表示foo的地址引用
a:undefined
}
未進(jìn)入執(zhí)行階段之前,變量對(duì)象中的屬性都不能訪(fǎng)問(wèn)!但是進(jìn)入執(zhí)行階段之后,變量對(duì)象轉(zhuǎn)變?yōu)榱嘶顒?dòng)對(duì)象,里面的屬性都能被訪(fǎng)問(wèn)了,然后開(kāi)始進(jìn)行執(zhí)行階段的操作。如果再面試的時(shí)候被問(wèn)到變量對(duì)象和活動(dòng)對(duì)象有什么區(qū)別,他們其實(shí)都是同一個(gè)對(duì)象,只是處于執(zhí)行上下文的不同生命周期。
//執(zhí)行階段
VO -->AO // Active Object
AO = {
arguments: {...},
foo: <foo reference>,
a: 1
}
再舉一個(gè)例子:
//創(chuàng)建階段
VO={
arguments: {...},
foo: <foo reference>,
bar: undefined
}
// 這里有一個(gè)需要注意的地方,因?yàn)関ar聲明的變量當(dāng)遇到同名的屬性時(shí),會(huì)跳過(guò)而不會(huì)覆蓋
// 執(zhí)行階段
VO -> AOVO = {
arguments: {...},
foo: 'Hello',
bar: <bar reference>
}