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

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

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

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

第一段代碼報錯很好理解,a 沒有聲明。所以拋出錯誤。

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

也就是說在使用 a 的時候,a 已經(jīng)被聲明了。這就很奇怪了,明明 a 的聲明在這一行下面,為什么這個時候就已經(jīng)被聲明了呢?

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

執(zhí)行上下文

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

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

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

下面有一個例子

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 這兩個變量,f1、f2、f3內(nèi)部是三個函數(shù)執(zhí)行上下文(Eval 函數(shù)執(zhí)行上下文不是很常用,在這里不做介紹)。

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

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

執(zhí)行棧

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

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

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

我們在上面的代碼的執(zhí)行過程可以歸結(jié)為下面這個圖:

image

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

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

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

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

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

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

  • VO 變量對象(Variable Object)

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

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

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

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

注意:

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

我們看一個例子

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

    };
    function c() {

    }
}

foo(22);

在調(diào)用了 foo(22) 的時候,創(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)建需要注意以下幾點

  1. 創(chuàng)建階段的創(chuàng)建順序是:函數(shù)的形參聲明并賦值 ==>> 函數(shù)聲明 ==>> 變量聲明
  2. 創(chuàng)建階段處理函數(shù)重名和變量重名的策略不同,簡單來說就是函數(shù)優(yōu)先級高。
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(){}

變量提升

通過上面的介紹我們其實就知道了變量提升這一現(xiàn)象的出現(xiàn)的根本原因就是執(zhí)行上下文在創(chuàng)建的時候就會掃描上下文中的變量將其聲明出來,并設(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)建在變量對象中。因此可以訪問它。

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

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

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

  1. 為什么 bar 的值是 undefined?

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

參考

  1. What is the Execution Context & Stack in JavaScript?
  2. 前端基礎(chǔ)進(jìn)階(三):變量對象詳解
?著作權(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ù)。

相關(guān)閱讀更多精彩內(nèi)容

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