深入理解JavaScript執(zhí)行上下文、函數(shù)堆棧、提升的概念

本文內(nèi)容轉(zhuǎn)載自:
https://feclub.cn/post/content/ec_ecs_hosting
http://blog.csdn.net/hi_kevin/article/details/37761919

首先明確幾個(gè)概念:

EC函數(shù)執(zhí)行環(huán)境(或執(zhí)行上下文),Execution Context
ECS執(zhí)行環(huán)境棧,Execution Context Stack
VO變量對(duì)象,Variable Object
AO活動(dòng)對(duì)象,Active Object
scope chain作用域鏈

EC(執(zhí)行上下文)

每次當(dāng)控制器轉(zhuǎn)到ECMAScript可執(zhí)行代碼的時(shí)候,就會(huì)進(jìn)入到一個(gè)執(zhí)行上下文。

那什么是可執(zhí)行代碼呢?

可執(zhí)行代碼的類型

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

這種類型的代碼是在"程序"級(jí)處理的:例如加載外部的js文件或者本地"<script></script>"標(biāo)簽內(nèi)的代碼。全局代碼不包括任何function體內(nèi)的代碼。 這個(gè)是默認(rèn)的代碼運(yùn)行環(huán)境,一旦代碼被載入,引擎最先進(jìn)入的就是這個(gè)環(huán)境。

2、函數(shù)代碼(Function code)

任何一個(gè)函數(shù)體內(nèi)的代碼,但是需要注意的是,具體的函數(shù)體內(nèi)的代碼是不包括內(nèi)部函數(shù)的代碼。

3、Eval代碼(Eval code)

eval內(nèi)部的代碼

ECS(執(zhí)行環(huán)境棧)

我們用MDN上的一個(gè)例子來(lái)引入函數(shù)執(zhí)行棧的概念

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

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

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



當(dāng)瀏覽器首次載入你的腳本,它將默認(rèn)進(jìn)入全局執(zhí)行上下文。如果,你在你的全局代碼中調(diào)用一個(gè)函數(shù),你程序的時(shí)序?qū)⑦M(jìn)入被調(diào)用的函數(shù),并創(chuàng)建一個(gè)新的執(zhí)行上下文,并將新創(chuàng)建的上下文壓入執(zhí)行棧的頂部。

如果你調(diào)用當(dāng)前函數(shù)內(nèi)部的其他函數(shù),相同的事情會(huì)在此上演。代碼的執(zhí)行流程進(jìn)入內(nèi)部函數(shù),創(chuàng)建一個(gè)新的執(zhí)行上下文并把它壓入執(zhí)行棧的頂部。瀏覽器總會(huì)執(zhí)行位于棧頂?shù)膱?zhí)行上下文,一旦當(dāng)前上下文函數(shù)執(zhí)行結(jié)束,它將被從棧頂彈出,并將上下文控制權(quán)交給當(dāng)前的棧。這樣,堆棧中的上下文就會(huì)被依次執(zhí)行并且彈出堆棧,直到回到全局的上下文。請(qǐng)看下面一個(gè)例子:

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

上述goo被聲明后,通過(guò)()運(yùn)算符強(qiáng)制直接運(yùn)行了。函數(shù)代碼就是調(diào)用了其自身3次,每次是局部變量i增加1。每次goo函數(shù)被自身調(diào)用時(shí),就會(huì)有一個(gè)新的執(zhí)行上下文被創(chuàng)建。每當(dāng)一個(gè)上下文執(zhí)行完畢,該上下文就被彈出堆棧,回到上一個(gè)上下文,直到再次回到全局上下文。整個(gè)過(guò)程抽象如下圖:

由此可見(jiàn) ,對(duì)于執(zhí)行上下文這個(gè)抽象的概念,可以歸納為以下幾點(diǎn):

1、單線程
2、同步執(zhí)行
3、唯一的一個(gè)全局上下文
4、函數(shù)的執(zhí)行上下文的個(gè)數(shù)沒(méi)有限制
5、每次某個(gè)函數(shù)被調(diào)用,就會(huì)有個(gè)新的執(zhí)行上下文為其創(chuàng)建,即使是調(diào)用的自身函數(shù),也是如此

看到這里,想必大家都已經(jīng)深諳上述例子輸出結(jié)果的原因了,這里我大概繪了一個(gè)流程圖來(lái)幫助理解foo:

VO(變量對(duì)象)/AO(活動(dòng)對(duì)象)

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

變量對(duì)象(Variable object):是說(shuō)JS的執(zhí)行上下文中都有個(gè)對(duì)象用來(lái)存放執(zhí)行上下文中可被訪問(wèn)但是不能被delete的函數(shù)標(biāo)示符、形參、變量聲明等。它們會(huì)被掛在這個(gè)對(duì)象上,對(duì)象的屬性對(duì)應(yīng)它們的名字對(duì)象屬性的值對(duì)應(yīng)它們的值但這個(gè)對(duì)象是規(guī)范上或者說(shuō)是引擎實(shí)現(xiàn)上的不可在JS環(huán)境中訪問(wèn)到活動(dòng)對(duì)象。

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

EC建立的細(xì)節(jié)

1、創(chuàng)建階段【當(dāng)函數(shù)被調(diào)用,但未執(zhí)行任何其內(nèi)部代碼之前】

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

2、執(zhí)行階段

初始化變量的值和函數(shù)的引用,解釋/執(zhí)行代碼。


我們可以將每個(gè)執(zhí)行上下文抽象為一個(gè)對(duì)象,這個(gè)對(duì)象具有三個(gè)屬性

ECObj: {
    scopeChain: { /* 變量對(duì)象(variableObject)+ 所有父級(jí)執(zhí)行上下文的變量對(duì)象*/ }, 
    variableObject: { /*函數(shù) arguments/參數(shù),內(nèi)部變量和函數(shù)聲明 */ }, 
    this: {} 
}

解釋器執(zhí)行代碼的偽邏輯

1、查找調(diào)用函數(shù)的代碼。
2、執(zhí)行代碼之前,先進(jìn)入創(chuàng)建上下文階段:

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

3、激活/代碼執(zhí)行階段:

在當(dāng)前上下文上運(yùn)行/解釋函數(shù)代碼,并隨著代碼一行行執(zhí)行指派變量的值。

VO --- 對(duì)應(yīng)上述第二個(gè)階段

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

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

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

  • 函數(shù)的形參(當(dāng)進(jìn)入函數(shù)執(zhí)行上下文時(shí)) —— 變量對(duì)象的一個(gè)屬性,其屬性名就是形參的名字,其值就是實(shí)參的值;對(duì)于沒(méi)有傳遞的參數(shù),其值為undefined

  • 函數(shù)聲明(FunctionDeclaration, FD) —— 變量對(duì)象的一個(gè)屬性,其屬性名和值都是函數(shù)對(duì)象創(chuàng)建出來(lái)的;如果變量對(duì)象已經(jīng)包含了相同名字的屬性,則替換它的值

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

對(duì)于函數(shù)的形參沒(méi)有什么可說(shuō)的,主要看一下函數(shù)的聲明以及變量的聲明兩個(gè)部分:
1、如何理解函數(shù)聲明過(guò)程中如果變量對(duì)象已經(jīng)包含了相同名字的屬性,則替換它的值這句話?
看如下這段代碼:

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

根據(jù)上面的介紹,我們知道VO創(chuàng)建過(guò)程中,函數(shù)形參的優(yōu)先級(jí)是高于函數(shù)的聲明的,結(jié)果是函數(shù)體內(nèi)部聲明的function a(){}覆蓋了函數(shù)形參a的聲明,因此最后輸出a是一個(gè)function。
2、如何理解變量聲明過(guò)程中如果變量名和已經(jīng)聲明的函數(shù)名或者函數(shù)的參數(shù)名相同,則不會(huì)影響已經(jīng)存在的屬性這句話?

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

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

下面是幾個(gè)比較有趣的例子,當(dāng)做加餐小菜,大家細(xì)細(xì)品味。這里給出一句話當(dāng)做參考:

函數(shù)聲明比變量?jī)?yōu)先級(jí)要高,并且定義過(guò)程不會(huì)被變量覆蓋,除非是賦值

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 --- 對(duì)應(yīng)第三個(gè)階段

正如我們看到的,創(chuàng)建的過(guò)程僅負(fù)責(zé)處理定義屬性的名字,而并不為他們指派具體的值,當(dāng)然還有對(duì)形參/實(shí)參的處理。一旦創(chuàng)建階段完成,執(zhí)行流進(jìn)入函數(shù)并且激活/代碼執(zhí)行階段,看下函數(shù)執(zhí)行完成后的樣子:

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

提升(Hoisting)

對(duì)于下面的代碼,相信很多人都能一眼看出輸出結(jié)果,但是卻很少有人能給出為什么會(huì)產(chǎn)生這種輸出結(jié)果的解釋。

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

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

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

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

總結(jié):

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

參考

javascript 執(zhí)行環(huán)境,變量對(duì)象,作用域鏈What is the Execution Context & Stack in JavaScript?函數(shù)MDN

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,619評(píng)論 6 539
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,155評(píng)論 3 425
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 177,635評(píng)論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 63,539評(píng)論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,255評(píng)論 6 410
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 55,646評(píng)論 1 326
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,655評(píng)論 3 444
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 42,838評(píng)論 0 289
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,399評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,146評(píng)論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,338評(píng)論 1 372
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,893評(píng)論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,565評(píng)論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 34,983評(píng)論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 36,257評(píng)論 1 292
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 52,059評(píng)論 3 397
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,296評(píng)論 2 376

推薦閱讀更多精彩內(nèi)容