導(dǎo)讀
本片文章,在前人的基礎(chǔ)上,加上自己的理解,解釋一下JavaScript的代碼執(zhí)行過程,順道介紹一下執(zhí)行環(huán)境和閉包的相關(guān)概念。
分為兩部分。第一部分是了解執(zhí)行環(huán)境的相關(guān)概念,第二部分是通過實際代碼了解具體執(zhí)行過程中執(zhí)行環(huán)境的切換。
執(zhí)行環(huán)境
執(zhí)行環(huán)境的分類
- 1.全局執(zhí)行環(huán)境
是JS代碼開始運行時的默認環(huán)境(瀏覽器中為window對象)。全局執(zhí)行環(huán)境的變量對象始終都是作用域鏈中的最后一個對象。 - 2.函數(shù)執(zhí)行環(huán)境
當(dāng)某個函數(shù)被調(diào)用時,會先創(chuàng)建一個執(zhí)行環(huán)境及相應(yīng)的作用域鏈。然后使用arguments和其他命名參數(shù)的值來初始化執(zhí)行環(huán)境的變量對象。 - 3.使用eval()執(zhí)行代碼
沒有塊級作用域(本文不涉及ES6中
let等概念)
執(zhí)行上下文(執(zhí)行環(huán)境)的組成
執(zhí)行環(huán)境(execution context,EC)或稱之為執(zhí)行上下文,是JS中一個極為重要的概念。當(dāng)JavaScript代碼執(zhí)行時,會進入不同的執(zhí)行上下文,而每個執(zhí)行上下文的組成,基本如下:

- 變量對象(Variable object,VO): 變量對象,即包含變量的對象,除了我們無法訪問它外,和普通對象沒什么區(qū)別
- [[Scope]]屬性:數(shù)組。作用域鏈是一個由變量對象組成的帶頭結(jié)點的單向鏈表,其主要作用就是用來進行變量查找。而[[Scope]]屬性是一個指向這個鏈表頭節(jié)點的指針。
- this: 指向一個環(huán)境對象,注意是一個對象,而且是一個普通對象,而不是一個執(zhí)行環(huán)境。
若干執(zhí)行上下文會構(gòu)成一個執(zhí)行上下文棧(Execution context stack,ECS)。而所謂的執(zhí)行上下文棧,舉個例子,比如下面的代碼
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()
代碼首先進入Global Execution Context,然后依次進入outerFunc,innerFunc和foo的執(zhí)行上下文,執(zhí)行上下文棧就可以表示為:

執(zhí)行全局代碼時,會產(chǎn)生一個執(zhí)行上下文環(huán)境,每次調(diào)用函數(shù)都又會產(chǎn)生執(zhí)行上下文環(huán)境。當(dāng)函數(shù)調(diào)用完成時,這個上下文環(huán)境以及其中的數(shù)據(jù)都會被消除,再重新回到全局上下文環(huán)境。處于活動狀態(tài)的執(zhí)行上下文環(huán)境只有一個。

產(chǎn)生執(zhí)行上下文的兩個階段
當(dāng)一段JS代碼執(zhí)行的時候,JS解釋器會通過兩個階段去產(chǎn)生一個EC
- 創(chuàng)建階段(當(dāng)函數(shù)被調(diào)用,但是開始執(zhí)行函數(shù)內(nèi)部代碼之前)
- 創(chuàng)建變量對象VO
- 設(shè)置[[Scope]]屬性的值
- 設(shè)置this的值
- 激活/代碼執(zhí)行階段
- 初始化變量對象,即設(shè)置變量的值、函數(shù)的引用,然后解釋/執(zhí)行代碼。
創(chuàng)建變量對象VO過程
- 1.根據(jù)函數(shù)的參數(shù),創(chuàng)建并初始化arguments object
- 2.掃描函數(shù)內(nèi)部代碼,查找函數(shù)聲明(function declaration)
- 對于所有找到的函數(shù)聲明,將函數(shù)名和函數(shù)引用存入VO中
- 如果VO中已經(jīng)有同名函數(shù),那么就進行覆蓋
- 3.掃描函數(shù)內(nèi)部代碼,查找變量聲明(Variable declaration)
- 對于所有找到的變量聲明(通過var聲明),將變量名存入VO中,并初始化為undefined
- 如果變量名跟已經(jīng)聲明的形參或函數(shù)相同,則什么也不做
注:步驟2和3也稱為聲明提升(declaration hoisting)
通過一段代碼來了解JavaScript代碼的執(zhí)行
我們舉例說明,假如我們有一個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);
下面我們來一步一步說明解釋器是如何執(zhí)行這段代碼的:
1.創(chuàng)建全局上下文
首先,在解釋器眼中,global_var1、global_sum叫做全局變量,因為它們不屬于任何函數(shù)。local_var1叫做局部變量,因為它定義在函數(shù)global_function1內(nèi)部。global_function1叫做全局函數(shù),因為它沒有定義在任何函數(shù)內(nèi)部。
然后,解釋器開始掃描這段代碼,為執(zhí)行這段代碼做了一些準備工作——創(chuàng)建了一個全局上下文。
全局上下文,可以把它看成一個JavaScript對象,姑且稱之為global_context。這個對象是解釋器創(chuàng)建的,當(dāng)然也是由解釋器使用。(我們的JavaScript代碼是接觸不到這個對象的)
global_context對象大概是這個樣子的:
global_context = {
Variable_Object :{......},
Scope :[......],
this :{......}
}
可以看到,global_context有三個屬性
-
Variable_Object(以下簡稱VO)
{
global_var1:undefined
global_function1:函數(shù) global_function1的地址
global_sum:undefined
}解釋器在VO中記錄了變量全局變量
global_var1、global_sum,但它們的值現(xiàn)在是undefined的,還記錄了全局函數(shù)global_function1,但是沒有記錄局部變量local_var1。VO的原型是Object.prototype。 -
Scope數(shù)組中的內(nèi)容如下:
[ global_context.Variable_Object ]我們看到,Scope數(shù)組中只有一個對象,就是前面剛創(chuàng)建的對象VO。
-
this
this的值現(xiàn)在是undefined
global_context對象被解釋器壓入一個棧中,不妨叫這個棧為context_stack?,F(xiàn)在的context_stack是這樣的:

創(chuàng)建出global_context后,解釋器又偷偷摸摸干了一件事,它給global_function1設(shè)置了一個內(nèi)部屬性,也叫scope,它的值就是global_context中的scope!也就是說,現(xiàn)在:
global_function1.scope === [ global_context.Variable_Object ];
我們獲取不到global_function1的scope屬性的,只有解釋器自己能獲取到。
2.逐行執(zhí)行代碼
解釋器在創(chuàng)建了全局上下文后,就開始執(zhí)行這段代碼了。
第一句:
var global_var1 = 10;
解釋器會把VO中的global_var1屬性的值設(shè)為10?,F(xiàn)在global_context對象變成了這樣:
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對象時,它就已經(jīng)記錄好了該函數(shù),所以現(xiàn)在它什么也不用做。
第三句:
var global_sum = global_function1(10);
解釋器看到,我們在這里調(diào)用了函數(shù)global_function1(解釋器已經(jīng)提前在global_context的VO中記錄下了global_function1,所以它知道我們這里是一個函數(shù)調(diào)用),并且傳入了一個參數(shù)10,函數(shù)的返回結(jié)果賦值給了全局變量global_sum。
解釋器并沒有立即執(zhí)行函數(shù)中的代碼,因為它要為函數(shù)global_function1創(chuàng)建一個專門的context,我們叫它執(zhí)行上下文(execute_context)吧,因為每當(dāng)解釋器要執(zhí)行一個函數(shù)時,都會創(chuàng)建一個類似的context。
execute_context也是一個對象,并且與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相比,有以下幾點變化:
- VO
- 首先記錄了函數(shù)的形式參數(shù)parameter_a,并且給它賦值10,這個10就是我們調(diào)用函數(shù)時傳遞進去的。
- 然后記錄了函數(shù)體內(nèi)的局部變量local_var1,它的值還是undefined。
- 然后是一個arguments屬性,它的值是一個數(shù)組,里面只有一個10。
你可能疑惑,不是已經(jīng)在parameter_a中記錄了參數(shù)10了嗎,為什么解釋器還要搞一個arguments,再來記錄一遍呢?原因是如果我們這樣調(diào)用函數(shù):
global_function1(10,20,30);
在JavaScript中是不違法的。此時VO中的arguments會變成這樣:
arguments:[10,20,30]
parameter_a的值還是10??梢?,arguments是專門記錄我們傳進去的所有參數(shù)的。
- Scope
Scope屬性仍然是一個數(shù)組,只不過里面的元素多了個execute_context.Variable_Object,并且排在了global_context.Variable_Object前面。
解釋器是根據(jù)什么規(guī)則決定Scope中的內(nèi)容的呢?答案非常簡單:
execute_context.Scope = execute_context.Variable_Object + global_function1.scope。
也就是說,每當(dāng)要執(zhí)行一個函數(shù)時,解釋器都會將執(zhí)行上下文(execute_context)中Scope數(shù)組的第一個元素設(shè)為該執(zhí)行上下文(execute_context)的VO對象,然后取出函數(shù)創(chuàng)建時保存在函數(shù)中的scope屬性(本文中則是global_function1.scope),將其添加到執(zhí)行上下文(execute_context)Scope數(shù)組的后面。
我們知道,global_function1是在global_context下創(chuàng)建的,創(chuàng)建的時候,它的scope屬性被設(shè)置成了global_context的Scope,里面只有一個global_context.Variable_Object,于是這個對象被添加到execute_context.Scope數(shù)組中execute_context.Variable_Object對象后面。
任何一個函數(shù)在創(chuàng)建時,解釋器都會把它所在的執(zhí)行上下文或者全局上下文的Scope屬性對應(yīng)的數(shù)組設(shè)置給函數(shù)的scope屬性,這個屬性是函數(shù)“與生俱來”的。
- this
this的值此時仍然是undefined的(但不同的解釋器可能有不同的賦值)
解釋器為函數(shù)global_function1創(chuàng)建好了execute_context(執(zhí)行上下文)后,會把這個上下文對象壓入context_stack中,所以,現(xiàn)在的context_stack是這樣的:

準備執(zhí)行函數(shù)內(nèi)的代碼
做好了準備工作,解釋器開始執(zhí)行函數(shù)里面的代碼了,此時我們稱函數(shù)是在執(zhí)行上下文中運行的。
第一句
var local_var1 = 10 ;
它的處理辦法很簡單,將execute_context的VO中的local_var1賦值為10。這一點與在global_context下執(zhí)行的變量賦值語句的處理一樣。此時的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;
- 解釋器進一步考察語句,發(fā)現(xiàn)這是一個返回語句,于是它開始計算return 后面的表達式的值。
- 在表達式中它首先碰到了變量
local_var1,它首先在execute_context的Scope中依次查找,在第一個元素execute_context的VO發(fā)現(xiàn)了local_var1,并且知道它的值是10 - 然后解釋器繼續(xù)前進,碰到了變量
parameter_a,它如法炮制,在execute_context的VO中發(fā)現(xiàn)了parameter_a,并且確定它的值是10。 - 接著發(fā)現(xiàn)
global_var1,解釋器從execute_context的Scope第一個元素execute_context.VO中查找,沒有發(fā)現(xiàn)global_var1。繼續(xù)查看Scope數(shù)組的第二個元素,即global_context.VO,發(fā)現(xiàn)并且確定了它的值為10。 - 于是,解釋器將三個變量值相加得到了30,然后就返回了。
- 此時,解釋器知道函數(shù)已經(jīng)執(zhí)行完了,那么它為這個函數(shù)創(chuàng)建的執(zhí)行上下文也沒有用了,于是,它將execute_context從context_stack中彈出,由于沒有其他對象引用著execute_context,解釋器就把它銷毀了。現(xiàn)在context_stack中又只剩下了global_context。
第三句
var global_sum = 30;
現(xiàn)在解釋器又回到全局上下文中執(zhí)行代碼了,這時它要把30賦值給sum,方法就是更改global_context中的VO對象的global_sum屬性的值。
第四句
alert(global_sum);
解釋器繼續(xù)前進,碰到了語句alert(global_sum);很簡單,就是發(fā)出一個彈窗,彈窗的內(nèi)容就是global_sum的值30,當(dāng)我們點擊彈窗上的確定按鈕后,解釋器知道,這段代碼終于執(zhí)行完了,它會打掃戰(zhàn)場,把global_context,context_stack等資源全部銷毀。
再遇閉包
現(xiàn)在,知道了上下文,函數(shù)的scope屬性的知識后,我們就可以開始學(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)建了一個函數(shù)local_function1,并且將它作為返回值。
當(dāng)解釋器執(zhí)行函數(shù)global_function1時,仍然會為它創(chuàng)建執(zhí)行上下文,只不過此時execute_context.VO中多了一個函數(shù)屬性local_function1。然后,解釋器就會開始執(zhí)行global_function1中的代碼。
我們直接從創(chuàng)建local_function1語句開始分析,看解釋器是怎么執(zhí)行的,閉包的所有秘密就隱藏在其中。
當(dāng)解釋器在execute_context中執(zhí)行創(chuàng)建local_function1時,它仍然會將execute_context的Scope設(shè)置給函數(shù)local_function1的scope屬性,也就是這樣:
local_function1.scope = [ execute_context.Variable_Object, global_context.Variable_Object ]
然后,解釋器碰到了返回語句,把local_function1返回并賦值給了全局變量global_sum。此時global_context的VO中global_sum的值就是函數(shù)local_function1。
此時,函數(shù)global_function1已經(jīng)執(zhí)行完了,解釋器會怎么處理它的execute_context呢?
首先,解釋器會把execute_context從context_stack中彈出,但并不把它完全銷毀,而是保留了execute_context.Variable_Object對象,把它轉(zhuǎn)移到了另一塊堆內(nèi)存中。為什么不銷毀呢?因為還有對象引用著它呢。引用鏈如下:

這意味著什么呢?這說明,當(dāng)global_function1結(jié)束返回后,它的形式參數(shù)parameter_a,局部變量local_var1以及局部函數(shù)local_function1都沒有銷毀,還仍然存在。這一點,與面向?qū)ο蟮恼Z言Java中的經(jīng)驗完全不同,這也是閉包難以理解的根本所在。
下面我們的解釋器繼續(xù)執(zhí)行語句alert(global_sum(10));alert參數(shù)是對函數(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í)行這個函數(shù),解釋器仍然會為它創(chuàng)建一個執(zhí)行上下文,我們姑且稱之為local_context2,這個對象的內(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
}
這里我們重點看看Scope屬性,它的第一個元素毫無疑問是execute_context2.Variable_Object,后面的元素是從local_function1.scope屬性中獲得的,它是在local_function1創(chuàng)建時所在的執(zhí)行上下文的Scope屬性決定的。
創(chuàng)建的execute_context2壓入context_stack后,解釋器開始執(zhí)行語句
return parameter_b + local_var1 + parameter_a + global_var1;
對于該句中四個變量,解釋器確定它們的值的辦法一如既往的簡單,首先在當(dāng)前執(zhí)行上下文(也就是execute_context2)的Scope的第一個元素中查找,第一個找不到就在第二個元素中查找,然后就是第三個,直至global_context.Variable_Object。
然后,解釋器就會將四個變量值相加后返回。彈出execute_context2,此時execute_context2已經(jīng)沒有對象引用著它,解釋器就把它銷毀了。
最后,alert函數(shù)會收到值40,然后發(fā)出一個彈窗,彈窗的內(nèi)容就是40。程序結(jié)束
說到現(xiàn)在,啥是閉包啊?
簡單講,當(dāng)我們從函數(shù)global_function1中返回另一個函數(shù)local_function1時,由于local_function1的scope屬性中引用著為執(zhí)行global_function1創(chuàng)建的execute_context.Variable_Object對象,導(dǎo)致global_function1在執(zhí)行完畢后,它的execute_context.Variable_Object對象并不會被回收,此時我們稱函數(shù)local_function1是一個閉包,因為它除了是一個函數(shù)外,還保存著創(chuàng)建它的執(zhí)行上下文的變量信息,使得我們在調(diào)用它時,仍然能夠訪問這些變量。
函數(shù)將創(chuàng)建它的上下文中的VO對象封閉包含在自己的scope屬性中,函數(shù)就變成了一個閉包。從這個廣泛的意義上來說,global_function1也可以叫做閉包,因為它的scope內(nèi)部屬性也包含了創(chuàng)建它的全局上下文的變量信息,也就是global_context.VO