JavaScript 執(zhí)行上下文和執(zhí)行棧

執(zhí)行上下文和執(zhí)行棧

開始之前,我們先看以下代碼。

console.log(a)
// Uncaught ReferenceError: a is not defined
console.log(a)
// undefined
var a = 10

第一段代碼報(bào)錯(cuò)很好理解羽杰,a 沒有聲明。所以拋出錯(cuò)誤到推。

第二段代碼中 a 的聲明在使用 a 之后考赛,打印 a 的值是 undefined。

也就是說在使用 a 的時(shí)候莉测,a 已經(jīng)被聲明了颜骤。這就很奇怪了,明明 a 的聲明在這一行下面捣卤,為什么這個(gè)時(shí)候就已經(jīng)被聲明了呢忍抽?

其實(shí)這就是變量提升的概念。本質(zhì)上是因?yàn)楫?dāng)代碼真正執(zhí)行之前就已經(jīng)做了一些準(zhǔn)備工作董朝。而這些工作跟執(zhí)行上下文有著緊密的聯(lián)系鸠项,我們需要先來了解什么是執(zhí)行上下文。

執(zhí)行上下文

簡(jiǎn)單來說執(zhí)行上下文(Execution Context)就是執(zhí)行代碼的環(huán)境子姜。所有的代碼都在執(zhí)行上下文中執(zhí)行锈锤。

上面的例子都是在全局上下文中執(zhí)行的,其實(shí)執(zhí)行上下文可以分為下面這三種

  1. 全局執(zhí)行上下文 (Global Execution Context)
  • 這是最基礎(chǔ)或者默認(rèn)的執(zhí)行上下文闲询,是代碼一開始運(yùn)行就會(huì)創(chuàng)建的上下文。
  • 一個(gè)程序中只會(huì)有一個(gè)全局執(zhí)行上下文
  • 所有不在函數(shù)內(nèi)部的代碼都在全局執(zhí)行上下文之中
  1. 函數(shù)執(zhí)行上下文 (Functional Execution Context)
  • 當(dāng)一個(gè)函數(shù)被調(diào)用時(shí), 會(huì)為該函數(shù)創(chuàng)建一個(gè)上下文
  • 每個(gè)函數(shù)都有自己的執(zhí)行上下文
  1. Eval 函數(shù)執(zhí)行上下文 (Eval Function Execution Context)
  • 執(zhí)行在 eval 函數(shù)內(nèi)部的代碼也會(huì)有它屬于自己的執(zhí)行上下文

下面有一個(gè)例子

var v = 'global_context'
function f1() {
    var v1 = 'f1 context'
    function f2() {
        var v2 = 'f2 context'
        function f3() {
            var v3 = 'f3 context'
            console.log(v3)
        }
        f3()
        console.log(v2)
    }
    f2()
    console.log(v1)
}  
f1()
console.log(v)
image

最外側(cè)的是全局執(zhí)行上下文浅辙,它有 f1 和 v 這兩個(gè)變量扭弧,f1、f2记舆、f3內(nèi)部是三個(gè)函數(shù)執(zhí)行上下文(Eval 函數(shù)執(zhí)行上下文不是很常用鸽捻,在這里不做介紹)。

通過上面我們了解了每個(gè)函數(shù)都對(duì)應(yīng)一個(gè)執(zhí)行上下文,實(shí)際代碼中肯定會(huì)有很多的函數(shù)御蒲,甚至函數(shù)會(huì)嵌套函數(shù)衣赶,這些執(zhí)行上下文是如何組織起來的呢?代碼又是如何運(yùn)行的呢厚满?

其實(shí)這些都是執(zhí)行棧的工作府瞄。

執(zhí)行棧

執(zhí)行棧,其他語言中被稱為調(diào)用棧碘箍,與存儲(chǔ)變量的那個(gè)棧的概念不同遵馆,它是被用來存儲(chǔ)代碼運(yùn)行時(shí)創(chuàng)建的所有執(zhí)行上下文的棧。

當(dāng) JavaScript 引擎第一次遇到你的腳本時(shí)丰榴,它會(huì)創(chuàng)建一個(gè)全局的執(zhí)行上下文并且壓入當(dāng)前執(zhí)行棧货邓。每當(dāng)引擎遇到一個(gè)函數(shù)調(diào)用,它會(huì)為該函數(shù)創(chuàng)建一個(gè)新的執(zhí)行上下文并壓入棧的頂部四濒。

Javascript 是一門單線程的語言换况,這就意味著同一個(gè)時(shí)間只能處理一個(gè)任務(wù)。因此引擎只會(huì)執(zhí)行那些執(zhí)行上下文位于棧頂?shù)暮瘮?shù)盗蟆。當(dāng)該函數(shù)執(zhí)行結(jié)束時(shí)戈二,執(zhí)行上下文從棧中彈出,控制流程到達(dá)當(dāng)前棧中的下一個(gè)上下文姆涩。

我們?cè)谏厦娴拇a的執(zhí)行過程可以歸結(jié)為下面這個(gè)圖:

image

文字版總結(jié)如下:

  1. 全局上下文壓入棧頂
  2. 每執(zhí)行某一函數(shù)就為其創(chuàng)建一個(gè)執(zhí)行上下文挽拂,并壓入棧頂
  3. 棧頂?shù)暮瘮?shù)執(zhí)行完之后它的執(zhí)行上下文就會(huì)從執(zhí)行棧中彈出,將控制權(quán)交給下一個(gè)上下文
  4. 所有函數(shù)執(zhí)行完之后執(zhí)行棧中只剩下全局上下文骨饿,它會(huì)在應(yīng)用關(guān)閉時(shí)銷毀

執(zhí)行上下文的創(chuàng)建

如果執(zhí)行上下文抽象成為一個(gè)對(duì)象的話它是如下的對(duì)象

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

其中 variableObject 不是一成不變的,按照時(shí)間順序可以分為 VO 和 AO

  • VO 變量對(duì)象(Variable Object)

    • 它是執(zhí)行上下文中都有的對(duì)象宏赘。
    • 執(zhí)行上下文中可被訪問但是不能被 delete 的函數(shù)標(biāo)示符绒北、形參、變量聲明等都會(huì)被掛在這個(gè)對(duì)象上
    • 對(duì)象的屬性名對(duì)應(yīng)它們的名字察署,對(duì)象屬性的值對(duì)應(yīng)它們的值闷游。
    • 該對(duì)象不能直接訪問到
  • AO 活動(dòng)對(duì)象(Activation object)

    • 當(dāng)函數(shù)開始執(zhí)行的時(shí)候,這個(gè)執(zhí)行上下文兒中的變量對(duì)象就被激活贴汪,這時(shí)候 VO 就變成了 AO

因此執(zhí)行上下文創(chuàng)建的具體過程如下:

  1. 找到當(dāng)前上下文調(diào)用函數(shù)的代碼
  2. 執(zhí)行代碼之前脐往,先創(chuàng)建執(zhí)行上下文
  3. 創(chuàng)建階段:
    1. 創(chuàng)建變量對(duì)象:
      1. 創(chuàng)建 arguments 對(duì)象,和參數(shù)
      2. 掃描上下文的函數(shù)申明:
        1. 每掃描到一個(gè)函數(shù)什么就會(huì)用函數(shù)名創(chuàng)建一個(gè)屬性扳埂,它是一個(gè)指針业簿,指向該函數(shù)在內(nèi)存中的地址
        2. 如果函數(shù)名已經(jīng)存在,對(duì)應(yīng)的屬性值會(huì)被新的指針覆蓋
      3. 掃描上下文的變量申明:
        1. 每掃描到一個(gè)變量就會(huì)用變量名作為屬性名阳懂,其值初始化為 undefined
        2. 如果該變量名在變量對(duì)象中已經(jīng)存在梅尤,則直接跳過繼續(xù)掃描
    2. 初始化作用域鏈
    3. 確定上下文中 this 的指向
  4. 代碼執(zhí)行階段
    1. 執(zhí)行函數(shù)體中的代碼柜思,給變量賦值

注意:

  1. 全局上下文的變量對(duì)象初始化是全局對(duì)象
  2. 全局上下文的生命周期,與程序的生命周期一致巷燥,只要程序運(yùn)行不結(jié)束赡盘,比如關(guān)掉瀏覽器窗口,全局上下文就會(huì)一直存在缰揪。
  3. 作用域鏈(scopeChain) 和 this 的指向我們后面再詳細(xì)了解

我們看一個(gè)例子

function foo(i) {
    var a = 'hello';
    var b = function privateB() {

    };
    function c() {

    }
}

foo(22);

在調(diào)用了 foo(22) 的時(shí)候陨享,創(chuàng)建階段如下所示

fooExecutionContext = {
    scopeChain: { ... },
    variableObject: {
        arguments: {
            0: 22,
            length: 1
        },
        i: 22,
        c: pointer to function c()
        a: undefined,
        b: undefined
    },
    this: { ... }
}

激活階段如下

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

注意

創(chuàng)建需要注意以下幾點(diǎn)

  1. 創(chuàng)建階段的創(chuàng)建順序是:函數(shù)的形參聲明并賦值 ==>> 函數(shù)聲明 ==>> 變量聲明
  2. 創(chuàng)建階段處理函數(shù)重名和變量重名的策略不同,簡(jiǎn)單來說就是函數(shù)優(yōu)先級(jí)高邀跃。
function foo(a){
    console.log(a)
    var a = 10
}
foo(20) // 20

function foo(a){
    console.log(a)
    function a(){}
}
foo(20) // ? a(){}

function foo(){
    console.log(a)
    var a = 10
    function a(){}
}
foo() // f a(){}

變量提升

通過上面的介紹我們其實(shí)就知道了變量提升這一現(xiàn)象的出現(xiàn)的根本原因就是執(zhí)行上下文在創(chuàng)建的時(shí)候就會(huì)掃描上下文中的變量將其聲明出來霉咨,并設(shè)置為 VO 的屬性。

我們分析下面的代碼來加深印象拍屑。

(function() {

    console.log(typeof foo); // function pointer
    console.log(typeof bar); // undefined

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

    function foo() {
        return 'hello';
    }

}());?

我們來回答以下問題

  1. 為什么我們能在 foo 聲明之前訪問它途戒?

回想 VO 的創(chuàng)建階段,foo 在該階段就已經(jīng)被創(chuàng)建在變量對(duì)象中僵驰。因此可以訪問它喷斋。

  1. foo 被聲明了兩次, 為什么 foo 展現(xiàn)出來的是 functiton,而不是undefined 或者 string

在創(chuàng)建階段蒜茴,函數(shù)聲明是優(yōu)先于變量被創(chuàng)建的星爪。而且在變量的創(chuàng)建過程中,如果發(fā)現(xiàn) VO 中已經(jīng)存在相同名稱的屬性粉私,則不會(huì)影響已經(jīng)存在的屬性顽腾。

因此,對(duì) foo() 函數(shù)的引用首先被創(chuàng)建在活動(dòng)對(duì)象里诺核,并且當(dāng)我們解釋到 var foo 時(shí)抄肖,我們看見 foo 屬性名已經(jīng)存在,所以代碼什么都不做并繼續(xù)執(zhí)行窖杀。

  1. 為什么 bar 的值是 undefined漓摩?

bar 采用的是函數(shù)表達(dá)式的方式來定義的,所以 bar 實(shí)際上是一個(gè)變量入客,但變量的值是函數(shù)管毙,并且我們知道變量在創(chuàng)建階段被創(chuàng)建但他們被初始化為 undefined。

參考

  1. What is the Execution Context & Stack in JavaScript?
  2. 前端基礎(chǔ)進(jìn)階(三):變量對(duì)象詳解
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末桌硫,一起剝皮案震驚了整個(gè)濱河市夭咬,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌铆隘,老刑警劉巖皱埠,帶你破解...
    沈念sama閱讀 206,378評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異咖驮,居然都是意外死亡边器,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門托修,熙熙樓的掌柜王于貴愁眉苦臉地迎上來忘巧,“玉大人,你說我怎么就攤上這事睦刃⊙庾欤” “怎么了?”我有些...
    開封第一講書人閱讀 152,702評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵涩拙,是天一觀的道長(zhǎng)际长。 經(jīng)常有香客問我,道長(zhǎng)兴泥,這世上最難降的妖魔是什么工育? 我笑而不...
    開封第一講書人閱讀 55,259評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮搓彻,結(jié)果婚禮上如绸,老公的妹妹穿的比我還像新娘。我一直安慰自己旭贬,他們只是感情好怔接,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,263評(píng)論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著稀轨,像睡著了一般扼脐。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上奋刽,一...
    開封第一講書人閱讀 49,036評(píng)論 1 285
  • 那天瓦侮,我揣著相機(jī)與錄音,去河邊找鬼杨名。 笑死脏榆,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的台谍。 我是一名探鬼主播须喂,決...
    沈念sama閱讀 38,349評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼趁蕊!你這毒婦竟也來了坞生?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,979評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤掷伙,失蹤者是張志新(化名)和其女友劉穎是己,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體任柜,經(jīng)...
    沈念sama閱讀 43,469評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡卒废,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,938評(píng)論 2 323
  • 正文 我和宋清朗相戀三年沛厨,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片摔认。...
    茶點(diǎn)故事閱讀 38,059評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡逆皮,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出参袱,到底是詐尸還是另有隱情电谣,我是刑警寧澤,帶...
    沈念sama閱讀 33,703評(píng)論 4 323
  • 正文 年R本政府宣布抹蚀,位于F島的核電站剿牺,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏环壤。R本人自食惡果不足惜晒来,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,257評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望镐捧。 院中可真熱鬧潜索,春花似錦、人聲如沸懂酱。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽列牺。三九已至整陌,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間瞎领,已是汗流浹背泌辫。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留九默,地道東北人震放。 一個(gè)月前我還...
    沈念sama閱讀 45,501評(píng)論 2 354
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像驼修,于是被迫代替她去往敵國(guó)和親殿遂。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,792評(píng)論 2 345

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