深入理解JavaScript執行上下文、函數堆棧、提升的概念

本文內容轉載自:
https://feclub.cn/post/content/ec_ecs_hosting
http://blog.csdn.net/hi_kevin/article/details/37761919

首先明確幾個概念:

EC函數執行環境(或執行上下文),Execution Context
ECS執行環境棧,Execution Context Stack
VO變量對象,Variable Object
AO活動對象,Active Object
scope chain作用域鏈

EC(執行上下文)

每次當控制器轉到ECMAScript可執行代碼的時候,就會進入到一個執行上下文。

那什么是可執行代碼呢?

可執行代碼的類型

**1、全局代碼(Global code) **

這種類型的代碼是在"程序"級處理的:例如加載外部的js文件或者本地"<script></script>"標簽內的代碼。全局代碼不包括任何function體內的代碼。 這個是默認的代碼運行環境,一旦代碼被載入,引擎最先進入的就是這個環境。

2、函數代碼(Function code)

任何一個函數體內的代碼,但是需要注意的是,具體的函數體內的代碼是不包括內部函數的代碼。

3、Eval代碼(Eval code)

eval內部的代碼

ECS(執行環境棧)

我們用MDN上的一個例子來引入函數執行棧的概念

function foo(i) {
    if (i < 0) return;
    console.log('begin:' + i);
    foo(i - 1);
    console.log('end:' + i);
}
foo(2);

// 輸出:

// begin:2
// begin:1
// begin:0
// end:0
// end:1
// end:2

這里先不關心執行結果。磨刀不誤砍柴功,先了解一下函數執行上下文堆棧的概念。相信弄明白了下面的概念,一切也就水落石出了。

我們都知道,瀏覽器中的JS解釋器被實現為單線程,這也就意味著同一時間只能發生一件事情,其他的行為或事件將會被放在叫做執行棧里面排隊。下面的圖是單線程棧的抽象視圖:



當瀏覽器首次載入你的腳本,它將默認進入全局執行上下文。如果,你在你的全局代碼中調用一個函數,你程序的時序將進入被調用的函數,并創建一個新的執行上下文,并將新創建的上下文壓入執行棧的頂部。

如果你調用當前函數內部的其他函數,相同的事情會在此上演。代碼的執行流程進入內部函數,創建一個新的執行上下文并把它壓入執行棧的頂部。瀏覽器總會執行位于棧頂的執行上下文,一旦當前上下文函數執行結束,它將被從棧頂彈出,并將上下文控制權交給當前的棧。這樣,堆棧中的上下文就會被依次執行并且彈出堆棧,直到回到全局的上下文。請看下面一個例子:

(function goo(i){
   if(i === 3){
     return
  }else{
    goo(i++)
  }
}(0));

上述goo被聲明后,通過()運算符強制直接運行了。函數代碼就是調用了其自身3次,每次是局部變量i增加1。每次goo函數被自身調用時,就會有一個新的執行上下文被創建。每當一個上下文執行完畢,該上下文就被彈出堆棧,回到上一個上下文,直到再次回到全局上下文。整個過程抽象如下圖:

由此可見 ,對于執行上下文這個抽象的概念,可以歸納為以下幾點:

1、單線程
2、同步執行
3、唯一的一個全局上下文
4、函數的執行上下文的個數沒有限制
5、每次某個函數被調用,就會有個新的執行上下文為其創建,即使是調用的自身函數,也是如此

看到這里,想必大家都已經深諳上述例子輸出結果的原因了,這里我大概繪了一個流程圖來幫助理解foo:

VO(變量對象)/AO(活動對象)

這里為什么要用一個/呢?按照字面理解,AO其實就是被激活的VO,兩個其實是一個東西。下面引用知乎上的一段話,幫助理解一下。原文鏈接

變量對象(Variable object):是說JS的執行上下文中都有個對象用來存放執行上下文中可被訪問但是不能被delete的函數標示符、形參、變量聲明等。它們會被掛在這個對象上,對象的屬性對應它們的名字對象屬性的值對應它們的值但這個對象是規范上或者說是引擎實現上的不可在JS環境中訪問到活動對象。

激活對象(Activation object):有了變量對象存每個上下文中的東西,但是它什么時候能被訪問到呢?就是每進入一個執行上下文時,這個執行上下文兒中的變量對象就被激活,也就是該上下文中的函數標示符、形參、變量聲明等就可以被訪問到了。

EC建立的細節

1、創建階段【當函數被調用,但未執行任何其內部代碼之前】

1、 創建作用域鏈(Scope Chain)
2、 創建變量,函數和參數。
3、 求”this“的值

2、執行階段

初始化變量的值和函數的引用,解釋/執行代碼。


我們可以將每個執行上下文抽象為一個對象,這個對象具有三個屬性

ECObj: {
    scopeChain: { /* 變量對象(variableObject)+ 所有父級執行上下文的變量對象*/ }, 
    variableObject: { /*函數 arguments/參數,內部變量和函數聲明 */ }, 
    this: {} 
}

解釋器執行代碼的偽邏輯

1、查找調用函數的代碼。
2、執行代碼之前,先進入創建上下文階段:

第一步:初始化作用域鏈
第二步:創建變量對象:
    a.創建arguments對象,檢查上下文,初始化參數名稱和值并創建引用的復制。
    b.掃描上下文的函數聲明(而非函數表達式):
        1、為發現的每一個函數,在變量對象上創建一個屬性,確切的說是函數的名字,其有一個指向函數在內存中的引用。
        2、如果函數的名字已經存在,引用指針將被重寫。
    c.掃描上下文的變量聲明:
        1、為發現的每個變量聲明,在變量對象上創建一個屬性,就是變量的名字,并且將變量的值初始化為undefined
        2、如果變量的名字已經在變量對象里存在,將不會進行任何操作并繼續掃描。
第三步:求出上下文內部this的值。

3、激活/代碼執行階段:

在當前上下文上運行/解釋函數代碼,并隨著代碼一行行執行指派變量的值。

VO --- 對應上述第二個階段

function foo(i){
    var a = 'hello'
    var b = function(){}
    function c(){}
}
foo(22)

//當我們調用foo(22)時,整個創建階段是下面這樣的:
ECObj = {
    scopChain: {...},
    variableObject: {
        arguments: {
                0: 22,
                length: 1
        },
        i: 22,
        c: pointer to function c()
        a: undefined,
        b: undefined
    },
    this: { ... }
}

正如我們看到的,在上下文創建階段,VO的初始化過程如下(該過程是有先后順序的:函數的形參==>>函數聲明==>>變量聲明):

  • 函數的形參(當進入函數執行上下文時) —— 變量對象的一個屬性,其屬性名就是形參的名字,其值就是實參的值;對于沒有傳遞的參數,其值為undefined

  • 函數聲明(FunctionDeclaration, FD) —— 變量對象的一個屬性,其屬性名和值都是函數對象創建出來的;如果變量對象已經包含了相同名字的屬性,則替換它的值

  • 變量聲明(var,VariableDeclaration) —— 變量對象的一個屬性,其屬性名即為變量名,其值為undefined;如果變量名和已經聲明的函數名或者函數的參數名相同,則不會影響已經存在的屬性。

對于函數的形參沒有什么可說的,主要看一下函數的聲明以及變量的聲明兩個部分:
1、如何理解函數聲明過程中如果變量對象已經包含了相同名字的屬性,則替換它的值這句話?
看如下這段代碼:

function foo1(a){
    console.log(a)
    function a(){} 
}
foo1(20)//'function a(){}'

根據上面的介紹,我們知道VO創建過程中,函數形參的優先級是高于函數的聲明的,結果是函數體內部聲明的function a(){}覆蓋了函數形參a的聲明,因此最后輸出a是一個function。
2、如何理解變量聲明過程中如果變量名和已經聲明的函數名或者函數的參數名相同,則不會影響已經存在的屬性這句話?

//情景一:與參數名相同
function foo2(a){
    console.log(a)
    var a = 10
}
foo2(20) //'20'

//情景二:與函數名相同
function foo2(){
    console.log(a)
    var a = 10
    function a(){}
}
foo2() //'function a(){}'

下面是幾個比較有趣的例子,當做加餐小菜,大家細細品味。這里給出一句話當做參考:

函數聲明比變量優先級要高,并且定義過程不會被變量覆蓋,除非是賦值

function foo3(a){
    var a = 10
    function a(){}
    console.log(a)
}
foo3(20) //'10'

function foo3(a){
    var a 
    function a(){}
    console.log(a)
}
foo3(20) //'function a(){}'

AO --- 對應第三個階段

正如我們看到的,創建的過程僅負責處理定義屬性的名字,而并不為他們指派具體的值,當然還有對形參/實參的處理。一旦創建階段完成,執行流進入函數并且激活/代碼執行階段,看下函數執行完成后的樣子:

ECObj = {
    scopeChain: { ... },
    variableObject: {
        arguments: {
            0: 22,
            length: 1
        },
        i: 22,
        c: pointer to function c()
        a: 'hello',
        b: pointer to function privateB()
    },
    this: { ... }
}

提升(Hoisting)

對于下面的代碼,相信很多人都能一眼看出輸出結果,但是卻很少有人能給出為什么會產生這種輸出結果的解釋。

(function() {
    console.log(typeof foo); // 函數指針
    console.log(typeof bar); // undefined

    var foo = 'hello',
        bar = function() {
            return 'world';
        };

    function foo() {
        return 'hello';
    }
}());

1、為什么我們能在foo聲明之前訪問它?
回想在VO的創建階段,我們知道函數在該階段就已經被創建在變量對象中。所以在函數開始執行之前,foo已經被定義了。
2、foo被聲明了兩次,為什么foo顯示為函數而不是undefined或字符串?
我們知道,在創建階段,函數聲明是優先于變量被創建的。而且在變量的創建過程中,如果發現VO中已經存在相同名稱的屬性,則不會影響已經存在的屬性。因此,對foo()函數的引用首先被創建在活動對象里,并且當我們解釋到var foo時,我們看見foo屬性名已經存在,所以代碼什么都不做并繼續執行。
3、為什么bar的值是undefined?
bar采用的是函數表達式的方式來定義的,所以bar實際上是一個變量,但變量的值是函數,并且我們知道變量在創建階段被創建但他們被初始化為undefined,這也是為什么函數表達式不會被提升的原因。

總結:

1、EC分為兩個階段,創建執行上下文和執行代碼。
2、每個EC可以抽象為一個對象,這個對象具有三個屬性,分別為:作用域鏈Scope,VO|AO(AO,VO只能有一個)以及this。
3、函數EC中的AO在進入函數EC時,確定了Arguments對象的屬性;在執行函數EC時,其它變量屬性具體化。
4、EC創建的過程是由先后順序的:參數聲明 >函數聲明 >變量聲明。

參考

javascript 執行環境,變量對象,作用域鏈What is the Execution Context & Stack in JavaScript?函數MDN

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,882評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,208評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 175,746評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,666評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,477評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,960評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,047評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,200評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,726評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,617評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,807評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,327評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,049評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,425評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,674評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,432評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,769評論 2 372

推薦閱讀更多精彩內容