詳解JavaScript 論代碼執(zhí)行上下文

導(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í)行上下文的組成,基本如下:

image
  • 變量對象(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í)行上下文棧就可以表示為:

image

執(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)境只有一個。

image

產(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是這樣的:

image

創(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是這樣的:

image

準備執(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)存中。為什么不銷毀呢?因為還有對象引用著它呢。引用鏈如下:

image

這意味著什么呢?這說明,當(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_function1scope屬性中引用著為執(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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容