導(dǎo)讀
本片文章售淡,在前人的基礎(chǔ)上,加上自己的理解,解釋一下JavaScript的代碼執(zhí)行過(guò)程揖闸,順道介紹一下執(zhí)行環(huán)境和閉包的相關(guān)概念揍堕。
分為兩部分。第一部分是了解執(zhí)行環(huán)境的相關(guān)概念汤纸,第二部分是通過(guò)實(shí)際代碼了解具體執(zhí)行過(guò)程中執(zhí)行環(huán)境的切換衩茸。
執(zhí)行環(huán)境
執(zhí)行環(huán)境的分類
- 1.全局執(zhí)行環(huán)境
是JS代碼開(kāi)始運(yùn)行時(shí)的默認(rèn)環(huán)境(瀏覽器中為window對(duì)象)。全局執(zhí)行環(huán)境的變量對(duì)象始終都是作用域鏈中的最后一個(gè)對(duì)象贮泞。 - 2.函數(shù)執(zhí)行環(huán)境
當(dāng)某個(gè)函數(shù)被調(diào)用時(shí)楞慈,會(huì)先創(chuàng)建一個(gè)執(zhí)行環(huán)境及相應(yīng)的作用域鏈。然后使用arguments和其他命名參數(shù)的值來(lái)初始化執(zhí)行環(huán)境的變量對(duì)象啃擦。 - 3.使用eval()執(zhí)行代碼
沒(méi)有塊級(jí)作用域(本文不涉及ES6中
let
等概念)
執(zhí)行上下文(執(zhí)行環(huán)境)的組成
執(zhí)行環(huán)境(execution context囊蓝,EC)或稱之為執(zhí)行上下文,是JS中一個(gè)極為重要的概念议惰。當(dāng)JavaScript代碼執(zhí)行時(shí)慎颗,會(huì)進(jìn)入不同的執(zhí)行上下文,而每個(gè)執(zhí)行上下文的組成言询,基本如下:
- 變量對(duì)象(Variable object俯萎,VO): 變量對(duì)象,即包含變量的對(duì)象运杭,除了我們無(wú)法訪問(wèn)它外夫啊,和普通對(duì)象沒(méi)什么區(qū)別
- [[Scope]]屬性:數(shù)組。作用域鏈?zhǔn)且粋€(gè)由變量對(duì)象組成的帶頭結(jié)點(diǎn)的單向鏈表辆憔,其主要作用就是用來(lái)進(jìn)行變量查找撇眯。而[[Scope]]屬性是一個(gè)指向這個(gè)鏈表頭節(jié)點(diǎn)的指針。
- this: 指向一個(gè)環(huán)境對(duì)象虱咧,注意是一個(gè)對(duì)象熊榛,而且是一個(gè)普通對(duì)象,而不是一個(gè)執(zhí)行環(huán)境玄坦。
若干執(zhí)行上下文會(huì)構(gòu)成一個(gè)執(zhí)行上下文棧(Execution context stack,ECS)绘沉。而所謂的執(zhí)行上下文棧,舉個(gè)例子择懂,比如下面的代碼
var a = "global var";
function foo(){
console.log(a);
}
function outerFunc(){
var b = "var in outerFunc";
console.log(b);
function innerFunc(){
var c = "var in innerFunc";
console.log(c);
foo();
}
innerFunc();
}
outerFunc()
代碼首先進(jìn)入Global Execution Context赂弓,然后依次進(jìn)入outerFunc绑榴,innerFunc和foo的執(zhí)行上下文,執(zhí)行上下文棧就可以表示為:
執(zhí)行全局代碼時(shí)窃诉,會(huì)產(chǎn)生一個(gè)執(zhí)行上下文環(huán)境杨耙,每次調(diào)用函數(shù)都又會(huì)產(chǎn)生執(zhí)行上下文環(huán)境。當(dāng)函數(shù)調(diào)用完成時(shí)飘痛,這個(gè)上下文環(huán)境以及其中的數(shù)據(jù)都會(huì)被消除珊膜,再重新回到全局上下文環(huán)境。處于活動(dòng)狀態(tài)的執(zhí)行上下文環(huán)境只有一個(gè)。
產(chǎn)生執(zhí)行上下文的兩個(gè)階段
當(dāng)一段JS代碼執(zhí)行的時(shí)候,JS解釋器會(huì)通過(guò)兩個(gè)階段去產(chǎn)生一個(gè)EC
- 創(chuàng)建階段(當(dāng)函數(shù)被調(diào)用描沟,但是開(kāi)始執(zhí)行函數(shù)內(nèi)部代碼之前)
- 創(chuàng)建變量對(duì)象VO
- 設(shè)置[[Scope]]屬性的值
- 設(shè)置this的值
- 激活/代碼執(zhí)行階段
- 初始化變量對(duì)象饭寺,即設(shè)置變量的值、函數(shù)的引用厚脉,然后解釋/執(zhí)行代碼。
創(chuàng)建變量對(duì)象VO過(guò)程
- 1.根據(jù)函數(shù)的參數(shù),創(chuàng)建并初始化arguments object
- 2.掃描函數(shù)內(nèi)部代碼塑陵,查找函數(shù)聲明(function declaration)
- 對(duì)于所有找到的函數(shù)聲明,將函數(shù)名和函數(shù)引用存入VO中
- 如果VO中已經(jīng)有同名函數(shù)蜡励,那么就進(jìn)行覆蓋
- 3.掃描函數(shù)內(nèi)部代碼令花,查找變量聲明(Variable declaration)
- 對(duì)于所有找到的變量聲明(通過(guò)var聲明),將變量名存入VO中凉倚,并初始化為undefined
- 如果變量名跟已經(jīng)聲明的形參或函數(shù)相同兼都,則什么也不做
注:步驟2和3也稱為聲明提升(declaration hoisting)
通過(guò)一段代碼來(lái)了解JavaScript代碼的執(zhí)行
我們舉例說(shuō)明,假如我們有一個(gè)js文件稽寒,內(nèi)容如下:
var global_var1 = 10;
function global_function1(parameter_a){
var local_var1 = 10 ;
return local_var1 + parameter_a + global_var1;
}
var global_sum = global_function1(10);
alert(global_sum);
下面我們來(lái)一步一步說(shuō)明解釋器是如何執(zhí)行這段代碼的:
1.創(chuàng)建全局上下文
首先扮碧,在解釋器眼中,global_var1
瓦胎、global_sum
叫做全局變量芬萍,因?yàn)樗鼈儾粚儆谌魏魏瘮?shù)。local_var1
叫做局部變量搔啊,因?yàn)樗x在函數(shù)global_function1
內(nèi)部柬祠。global_function1叫做全局函數(shù),因?yàn)樗鼪](méi)有定義在任何函數(shù)內(nèi)部负芋。
然后漫蛔,解釋器開(kāi)始掃描這段代碼嗜愈,為執(zhí)行這段代碼做了一些準(zhǔn)備工作——創(chuàng)建了一個(gè)全局上下文。
全局上下文莽龟,可以把它看成一個(gè)JavaScript對(duì)象蠕嫁,姑且稱之為global_context
。這個(gè)對(duì)象是解釋器創(chuàng)建的毯盈,當(dāng)然也是由解釋器使用剃毒。(我們的JavaScript代碼是接觸不到這個(gè)對(duì)象的)
global_context對(duì)象大概是這個(gè)樣子的:
global_context = {
Variable_Object :{......},
Scope :[......],
this :{......}
}
可以看到,global_context有三個(gè)屬性
-
Variable_Object(以下簡(jiǎn)稱VO)
{
global_var1:undefined
global_function1:函數(shù) global_function1的地址
global_sum:undefined
}解釋器在VO中記錄了變量全局變量
global_var1
搂赋、global_sum
赘阀,但它們的值現(xiàn)在是undefined
的,還記錄了全局函數(shù)global_function1
脑奠,但是沒(méi)有記錄局部變量local_var1
基公。VO的原型是Object.prototype
。 -
Scope數(shù)組中的內(nèi)容如下:
[ global_context.Variable_Object ]
我們看到宋欺,Scope數(shù)組中只有一個(gè)對(duì)象轰豆,就是前面剛創(chuàng)建的對(duì)象VO。
-
this
this的值現(xiàn)在是undefined
global_context對(duì)象被解釋器壓入一個(gè)棧中齿诞,不妨叫這個(gè)棧為context_stack∷嵝荩現(xiàn)在的context_stack是這樣的:
創(chuàng)建出global_context后,解釋器又偷偷摸摸干了一件事掌挚,它給global_function1設(shè)置了一個(gè)內(nèi)部屬性雨席,也叫scope,它的值就是global_context中的scope吠式!也就是說(shuō)陡厘,現(xiàn)在:
global_function1.scope === [ global_context.Variable_Object ];
我們獲取不到global_function1的scope屬性的,只有解釋器自己能獲取到特占。
2.逐行執(zhí)行代碼
解釋器在創(chuàng)建了全局上下文后糙置,就開(kāi)始執(zhí)行這段代碼了。
第一句:
var global_var1 = 10;
解釋器會(huì)把VO中的global_var1屬性的值設(shè)為10∈悄浚現(xiàn)在global_context對(duì)象變成了這樣:
global_context = {
Variable_Object :{
global_var1:10,
global_function1:函數(shù) global_function1的地址,
global_sum:undefined
},
Scope :[ global_context.Variable_Object ],
this :undefined
}
第二句:
解釋器繼續(xù)執(zhí)行我們的代碼谤饭,它碰到了聲明式函數(shù)global_function1,由于在創(chuàng)建global_context對(duì)象時(shí)懊纳,它就已經(jīng)記錄好了該函數(shù)揉抵,所以現(xiàn)在它什么也不用做。
第三句:
var global_sum = global_function1(10);
解釋器看到嗤疯,我們?cè)谶@里調(diào)用了函數(shù)global_function1(
解釋器已經(jīng)提前在global_context
的VO中記錄下了global_function1
冤今,所以它知道我們這里是一個(gè)函數(shù)調(diào)用),并且傳入了一個(gè)參數(shù)10
茂缚,函數(shù)的返回結(jié)果賦值給了全局變量global_sum
戏罢。
解釋器并沒(méi)有立即執(zhí)行函數(shù)中的代碼屋谭,因?yàn)樗獮楹瘮?shù)global_function1創(chuàng)建一個(gè)專門(mén)的context,我們叫它執(zhí)行上下文
(execute_context)吧龟糕,因?yàn)槊慨?dāng)解釋器要執(zhí)行一個(gè)函數(shù)時(shí)桐磁,都會(huì)創(chuàng)建一個(gè)類似的context。
execute_context
也是一個(gè)對(duì)象讲岁,并且與global_context
還很像我擂,下面是它里面的內(nèi)容:
execute_context = {
Variable_Object :{
parameter_a:10,
local_var1:undefined,
arguments:[10]
},
Scope :[execute_context.Variable_Object, global_context.Variable_Object ],
this :undefined
}
我們看到,execute_context與global_context相比催首,有以下幾點(diǎn)變化:
- VO
- 首先記錄了函數(shù)的形式參數(shù)parameter_a扶踊,并且給它賦值10,這個(gè)10就是我們調(diào)用函數(shù)時(shí)傳遞進(jìn)去的郎任。
- 然后記錄了函數(shù)體內(nèi)的局部變量local_var1,它的值還是undefined备籽。
- 然后是一個(gè)arguments屬性舶治,它的值是一個(gè)數(shù)組,里面只有一個(gè)10车猬。
你可能疑惑霉猛,不是已經(jīng)在parameter_a中記錄了參數(shù)10了嗎,為什么解釋器還要搞一個(gè)arguments珠闰,再來(lái)記錄一遍呢惜浅?原因是如果我們這樣調(diào)用函數(shù):
global_function1(10,20,30);
在JavaScript中是不違法的。此時(shí)VO中的arguments會(huì)變成這樣:
arguments:[10,20,30]
parameter_a的值還是10伏嗜√诚ぃ可見(jiàn),arguments是專門(mén)記錄我們傳進(jìn)去的所有參數(shù)的承绸。
- Scope
Scope屬性仍然是一個(gè)數(shù)組裸影,只不過(guò)里面的元素多了個(gè)execute_context.Variable_Object,并且排在了global_context.Variable_Object前面军熏。
解釋器是根據(jù)什么規(guī)則決定Scope中的內(nèi)容的呢轩猩?答案非常簡(jiǎn)單:
execute_context.Scope = execute_context.Variable_Object + global_function1.scope。
也就是說(shuō)荡澎,每當(dāng)要執(zhí)行一個(gè)函數(shù)時(shí)均践,解釋器都會(huì)將執(zhí)行上下文(execute_context)中Scope數(shù)組的第一個(gè)元素設(shè)為該執(zhí)行上下文(execute_context)的VO對(duì)象,然后取出函數(shù)創(chuàng)建時(shí)保存在函數(shù)中的scope屬性(本文中則是global_function1.scope)摩幔,將其添加到執(zhí)行上下文(execute_context)Scope數(shù)組的后面彤委。
我們知道,global_function1是在global_context下創(chuàng)建的热鞍,創(chuàng)建的時(shí)候葫慎,它的scope屬性被設(shè)置成了global_context的Scope衔彻,里面只有一個(gè)global_context.Variable_Object,于是這個(gè)對(duì)象被添加到execute_context.Scope數(shù)組中execute_context.Variable_Object對(duì)象后面偷办。
任何一個(gè)函數(shù)在創(chuàng)建時(shí)艰额,解釋器都會(huì)把它所在的執(zhí)行上下文或者全局上下文的Scope屬性對(duì)應(yīng)的數(shù)組設(shè)置給函數(shù)的scope屬性,這個(gè)屬性是函數(shù)“與生俱來(lái)”的椒涯。
- this
this的值此時(shí)仍然是undefined的(但不同的解釋器可能有不同的賦值)
解釋器為函數(shù)global_function1創(chuàng)建好了execute_context(執(zhí)行上下文)后柄沮,會(huì)把這個(gè)上下文對(duì)象壓入context_stack中,所以废岂,現(xiàn)在的context_stack是這樣的:
準(zhǔn)備執(zhí)行函數(shù)內(nèi)的代碼
做好了準(zhǔn)備工作祖搓,解釋器開(kāi)始執(zhí)行函數(shù)里面的代碼了,此時(shí)我們稱函數(shù)是在執(zhí)行上下文中運(yùn)行的湖苞。
第一句
var local_var1 = 10 ;
它的處理辦法很簡(jiǎn)單拯欧,將execute_context的VO中的local_var1賦值為10。這一點(diǎn)與在global_context下執(zhí)行的變量賦值語(yǔ)句的處理一樣财骨。此時(shí)的execute_context變成這樣:
execute_context = {
Variable_Object :{
parameter_a:10,
local_var1:10, //為local_var1賦值10
arguments:[10]
},
Scope :[execute_context.Variable_Object, global_context.Variable_Object ],
this :undefined
}
第二句
return local_var1 + parameter_a + global_var1;
- 解釋器進(jìn)一步考察語(yǔ)句镐作,發(fā)現(xiàn)這是一個(gè)返回語(yǔ)句,于是它開(kāi)始計(jì)算return 后面的表達(dá)式的值隆箩。
- 在表達(dá)式中它首先碰到了變量
local_var1
该贾,它首先在execute_context
的Scope中依次查找,在第一個(gè)元素execute_context
的VO發(fā)現(xiàn)了local_var1
捌臊,并且知道它的值是10 - 然后解釋器繼續(xù)前進(jìn)杨蛋,碰到了變量
parameter_a
,它如法炮制理澎,在execute_context
的VO中發(fā)現(xiàn)了parameter_a
逞力,并且確定它的值是10。 - 接著發(fā)現(xiàn)
global_var1
矾端,解釋器從execute_context
的Scope第一個(gè)元素execute_context.VO中查找掏击,沒(méi)有發(fā)現(xiàn)global_var1
。繼續(xù)查看Scope數(shù)組的第二個(gè)元素秩铆,即global_context.VO
砚亭,發(fā)現(xiàn)并且確定了它的值為10。 - 于是殴玛,解釋器將三個(gè)變量值相加得到了30捅膘,然后就返回了。
- 此時(shí)滚粟,解釋器知道函數(shù)已經(jīng)執(zhí)行完了寻仗,那么它為這個(gè)函數(shù)創(chuàng)建的執(zhí)行上下文也沒(méi)有用了,于是凡壤,它將execute_context從context_stack中彈出署尤,由于沒(méi)有其他對(duì)象引用著execute_context耙替,解釋器就把它銷毀了。現(xiàn)在context_stack中又只剩下了global_context曹体。
第三句
var global_sum = 30;
現(xiàn)在解釋器又回到全局上下文中執(zhí)行代碼了俗扇,這時(shí)它要把30賦值給sum,方法就是更改global_context
中的VO對(duì)象的global_sum
屬性的值箕别。
第四句
alert(global_sum);
解釋器繼續(xù)前進(jìn)铜幽,碰到了語(yǔ)句alert(global_sum);很簡(jiǎn)單,就是發(fā)出一個(gè)彈窗串稀,彈窗的內(nèi)容就是global_sum的值30除抛,當(dāng)我們點(diǎn)擊彈窗上的確定按鈕后,解釋器知道母截,這段代碼終于執(zhí)行完了到忽,它會(huì)打掃戰(zhàn)場(chǎng),把global_context微酬,context_stack等資源全部銷毀绘趋。
再遇閉包
現(xiàn)在,知道了上下文颗管,函數(shù)的scope屬性的知識(shí)后,我們就可以開(kāi)始學(xué)習(xí)閉包了滓走。讓我們將上面的js代碼改成這樣:
var global_var1 = 10;
function global_function1(parameter_a){
var local_var1 = 10 ;
function local_function1(parameter_b){
return parameter_b + local_var1 + parameter_a + global_var1;
}
return local_function1 ;
}
var global_sum = global_function1(10);
alert(global_sum(10));
這段代碼與原先的代碼最大的不同是垦江,在global_function1
內(nèi)部,我們創(chuàng)建了一個(gè)函數(shù)local_function1
搅方,并且將它作為返回值比吭。
當(dāng)解釋器執(zhí)行函數(shù)global_function1
時(shí),仍然會(huì)為它創(chuàng)建執(zhí)行上下文姨涡,只不過(guò)此時(shí)execute_context.VO
中多了一個(gè)函數(shù)屬性local_function1
衩藤。然后,解釋器就會(huì)開(kāi)始執(zhí)行global_function1
中的代碼涛漂。
我們直接從創(chuàng)建local_function1
語(yǔ)句開(kāi)始分析赏表,看解釋器是怎么執(zhí)行的,閉包的所有秘密就隱藏在其中匈仗。
當(dāng)解釋器在execute_context
中執(zhí)行創(chuàng)建local_function1
時(shí)瓢剿,它仍然會(huì)將execute_context
的Scope設(shè)置給函數(shù)local_function1
的scope屬性,也就是這樣:
local_function1.scope = [ execute_context.Variable_Object, global_context.Variable_Object ]
然后悠轩,解釋器碰到了返回語(yǔ)句间狂,把local_function1
返回并賦值給了全局變量global_sum
。此時(shí)global_context
的VO中global_sum
的值就是函數(shù)local_function1
火架。
此時(shí)鉴象,函數(shù)global_function1
已經(jīng)執(zhí)行完了忙菠,解釋器會(huì)怎么處理它的execute_context
呢?
首先纺弊,解釋器會(huì)把execute_context從context_stack中彈出牛欢,但并不把它完全銷毀,而是保留了execute_context.Variable_Object對(duì)象俭尖,把它轉(zhuǎn)移到了另一塊堆內(nèi)存中氢惋。為什么不銷毀呢?因?yàn)檫€有對(duì)象引用著它呢稽犁。引用鏈如下:
這意味著什么呢焰望?這說(shuō)明,當(dāng)global_function1
結(jié)束返回后已亥,它的形式參數(shù)parameter_a
熊赖,局部變量local_var1
以及局部函數(shù)local_function1
都沒(méi)有銷毀,還仍然存在虑椎。這一點(diǎn)震鹉,與面向?qū)ο蟮恼Z(yǔ)言Java中的經(jīng)驗(yàn)完全不同,這也是閉包難以理解的根本所在捆姜。
下面我們的解釋器繼續(xù)執(zhí)行語(yǔ)句alert(global_sum(10))
;alert參數(shù)是對(duì)函數(shù)global_sum
的調(diào)用传趾,global_sum
的參數(shù)為10,我們知道函數(shù)global_sum
的代碼是這樣的:
function local_function1(parameter_b){
return parameter_b + local_var1 + parameter_a + global_var1;
}
要執(zhí)行這個(gè)函數(shù)泥技,解釋器仍然會(huì)為它創(chuàng)建一個(gè)執(zhí)行上下文浆兰,我們姑且稱之為local_context2
,這個(gè)對(duì)象的內(nèi)容是這樣的:
execute_context2 = {
Variable_Object :{
parameter_b:10,
arguments:[10]
},
Scope :[execute_context2.Variable_Object, execute_context.Variable_Object, global_context.Variable_Object ],
this :undefined
}
這里我們重點(diǎn)看看Scope屬性珊豹,它的第一個(gè)元素毫無(wú)疑問(wèn)是execute_context2.Variable_Object
簸呈,后面的元素是從local_function1.scope
屬性中獲得的,它是在local_function1
創(chuàng)建時(shí)所在的執(zhí)行上下文的Scope屬性決定的店茶。
創(chuàng)建的execute_context2
壓入context_stack
后蜕便,解釋器開(kāi)始執(zhí)行語(yǔ)句
return parameter_b + local_var1 + parameter_a + global_var1;
對(duì)于該句中四個(gè)變量,解釋器確定它們的值的辦法一如既往的簡(jiǎn)單贩幻,首先在當(dāng)前執(zhí)行上下文(也就是execute_context2)的Scope的第一個(gè)元素中查找轿腺,第一個(gè)找不到就在第二個(gè)元素中查找,然后就是第三個(gè)段直,直至global_context.Variable_Object吃溅。
然后,解釋器就會(huì)將四個(gè)變量值相加后返回鸯檬。彈出execute_context2
决侈,此時(shí)execute_context2
已經(jīng)沒(méi)有對(duì)象引用著它,解釋器就把它銷毀了。
最后赖歌,alert函數(shù)會(huì)收到值40枉圃,然后發(fā)出一個(gè)彈窗,彈窗的內(nèi)容就是40庐冯。程序結(jié)束
說(shuō)到現(xiàn)在孽亲,啥是閉包啊展父?
簡(jiǎn)單講返劲,當(dāng)我們從函數(shù)global_function1
中返回另一個(gè)函數(shù)local_function1
時(shí),由于local_function1
的scope
屬性中引用著為執(zhí)行global_function1
創(chuàng)建的execute_context.Variable_Object
對(duì)象栖茉,導(dǎo)致global_function1
在執(zhí)行完畢后篮绿,它的execute_context.Variable_Object
對(duì)象并不會(huì)被回收,此時(shí)我們稱函數(shù)local_function1
是一個(gè)閉包吕漂,因?yàn)樗耸且粋€(gè)函數(shù)外亲配,還保存著創(chuàng)建它的執(zhí)行上下文的變量信息,使得我們?cè)谡{(diào)用它時(shí)惶凝,仍然能夠訪問(wèn)這些變量吼虎。
函數(shù)將創(chuàng)建它的上下文中的VO對(duì)象封閉包含在自己的scope屬性中,函數(shù)就變成了一個(gè)閉包苍鲜。從這個(gè)廣泛的意義上來(lái)說(shuō)思灰,global_function1
也可以叫做閉包,因?yàn)樗膕cope內(nèi)部屬性也包含了創(chuàng)建它的全局上下文的變量信息混滔,也就是global_context.VO