【你不知道的JavaScript】(三)執(zhí)行上下文及其生命周期

一、執(zhí)行上下文概念

JavaScript代碼的執(zhí)行過程分為兩個階段:

  • 代碼編譯階段:由編譯器完成,將代碼翻譯成可執(zhí)行代碼
  • 代碼執(zhí)行階段:由引擎完成,主要任務(wù)是執(zhí)行可執(zhí)行代碼

其中可執(zhí)行代碼分為三種:全局代碼函數(shù)代碼、eval代碼

有關(guān)JavaScript代碼的執(zhí)行過程可查看《【你不知道的JavaScript】(一)作用域與詞法作用域》一文。

簡單來說,當在代碼執(zhí)行階段執(zhí)行到一個函數(shù)的時候,就會進行準備工作,這里的“準備工作”,就叫做"執(zhí)行上下文(EC)",也叫執(zhí)行上下文環(huán)境,也叫執(zhí)行環(huán)境。

JavaScript代碼執(zhí)行時,會進入不同的執(zhí)行上下文,而每個執(zhí)行上下文的組成,基本如下:

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

二、執(zhí)行上下文生命周期

當調(diào)用一個函數(shù)時(激活),一個新的執(zhí)行上下文就會被創(chuàng)建。而一個執(zhí)行上下文的生命周期可以分為兩個階段:

  • 創(chuàng)建階段:在這個階段中,執(zhí)行上下文會分別創(chuàng)建變量對象,建立作用域鏈,以及確定this的指向。
  • 執(zhí)行階段:創(chuàng)建完成之后,就會開始執(zhí)行代碼,這個時候,會完成變量賦值,函數(shù)引用,以及執(zhí)行其他代碼。
執(zhí)行上下文生命周期

詳細了解執(zhí)行上下文極為重要,因為其中涉及到了變量對象,作用域鏈,this等極為重要的概念,它關(guān)系到我們能不能真正理解JavaScript,下面我們分別了解幾個概念。

(一)變量對象

1. 變量對象的創(chuàng)建過程

(1) 建立arguments對象。檢查當前上下文中的參數(shù),建立該對象下的屬性與屬性值。

(2) 檢查當前上下文的函數(shù)聲明,也就是使用function關(guān)鍵字聲明的函數(shù)。在變量對象中以函數(shù)名建立一個屬性,屬性值為指向該函數(shù)所在內(nèi)存地址的引用。如果函數(shù)名的屬性已經(jīng)存在,那么該屬性將會被新的引用所覆蓋。

(3) 檢查當前上下文中的變量聲明,每找到一個變量聲明,就在變量對象中以變量名建立一個屬性,屬性值為undefined。如果該變量名的屬性已經(jīng)存在,為了防止同名的函數(shù)被修改為undefined,則會直接跳過,原屬性值不會被修改。

變量對象創(chuàng)建過程
function foo() { console.log('function foo') }
var foo = 20;

console.log(foo); // 20

// ↑以上代碼中,變量聲明的 foo 遇到函數(shù)聲明的 foo 會跳過,
// 可是為什么最后 foo 的輸出結(jié)果仍然是被覆蓋了呢?
// 那是因為三條規(guī)則僅僅適用于變量對象的創(chuàng)建過程,也就是執(zhí)行上下文的創(chuàng)建過程。
// 而 foo=20 是在執(zhí)行上下文的執(zhí)行過程中運行的,輸出結(jié)果自然會是20。

再來看另外一個例子:

console.log(foo); // ? foo() { console.log('function foo') }
function foo() { console.log('function foo') }
var foo = 20;

// 上栗的執(zhí)行順序為
// 首先將所有函數(shù)聲明放入變量對象中
function foo() { console.log('function foo') }

// 其次將所有變量聲明放入變量對象中,
// 但是因為foo已經(jīng)存在同名函數(shù),因此此時會跳過undefined的賦值
// var foo = undefined;

// 然后開始執(zhí)行階段代碼的執(zhí)行
console.log(foo); // function foo
foo = 20;

2. 變量對象與活動對象

變量對象與活動對象其實都是同一個對象,只是處于執(zhí)行上下文的不同生命周期。不過只有處于函數(shù)調(diào)用棧棧頂?shù)膱?zhí)行上下文中的變量對象,才會變成活動對象。

function test() {
    console.log(a);
    console.log(foo());

    var a = 1;
    function foo() {
        return 2;
    }
}

test();

↑以上代碼中,全局作用域中運行test()時,test()的執(zhí)行上下文開始創(chuàng)建。為了便于理解,我們用如下的形式來表示:

// 創(chuàng)建過程
testEC = {
    // VO 為 Variable Object的縮寫,即變量對象
    VO: {
        //注:在瀏覽器的展示中,函數(shù)的參數(shù)可能并不是放在arguments對象中,
        //這里為了方便理解,我做了這樣的處理
        arguments: {...},  
        foo: <foo reference>,  // 表示 foo 的地址引用
        a: undefined,
        this: Window
    },
    scopeChain: {}
}

未進入執(zhí)行階段之前,變量對象中的屬性都不能訪問!但是進入執(zhí)行階段之后,變量對象轉(zhuǎn)變?yōu)榱嘶顒訉ο?/strong>,里面的屬性都能被訪問了,然后開始進行執(zhí)行階段的操作。

// 執(zhí)行階段
VO ->  AO   // Active Object
AO = {
    arguments: {...},
    foo: <foo reference>,
    a: 1,
    this: Window
}

因此,上面例子的執(zhí)行順序如下:

function test() {
    function foo() {
        return 2;
    }
    var a;
    console.log(a); // undefined
    console.log(foo()); // 2
    a = 1;
}

test();

3. 全局上下文的變量對象

全局上下文有一個特殊的地方,它的變量對象,就是window對象。而這個特殊,在this指向上也同樣適用,this也是指向window。

除此之外,全局上下文的生命周期,與程序的生命周期一致,只要程序運行不結(jié)束,比如關(guān)掉瀏覽器窗口,全局上下文就會一直存在。其他所有的上下文環(huán)境,都能直接訪問全局上下文的屬性。

(二)作用域鏈

作用域鏈本質(zhì)上是一個指向當前環(huán)境與上層環(huán)境的一系列變量對象的指針列表(它只引用但不實際包含變量對象),作用域鏈保證了當前執(zhí)行環(huán)境對符合訪問權(quán)限的變量和函數(shù)的有序訪問。

var a = 1;             
function out() {
    var b = 2;
    function inner() {
        var c = 3;
        console.log(a+b+c);
    }
    inner();          
}
out();

首先,代碼開始運行時就創(chuàng)建了全局上下文環(huán)境,接著運行到out()時創(chuàng)建 out函數(shù)的執(zhí)行上下文,最后運行到inner()時創(chuàng)建 inner函數(shù)的執(zhí)行上下文,我們設(shè)定他們的變量對象分別為VO(global),VO(out), VO(inner)。

我們可以直接用一個數(shù)組來表示作用域鏈,數(shù)組的第一項scopeChain[0]為作用域鏈的最前端,而數(shù)組的最后一項,為作用域鏈的最末端,所有的最末端都為全局變量對象。

  1. 全局的作用域鏈:由于它只含全局作用域,沒有上級,因此它的作用域鏈只指向本身的全局變量對象。查找標識符時只能從本身的全局變量對象中查找。
// 全局上下文環(huán)境
globalEC = {
    VO: {
        out: <out reference>,  // 表示 out 的地址引用
        a: undefined
    },
    scopeChain: [VO(global)], // 作用域鏈
}
  1. 函數(shù)out的作用域鏈:可以引用函數(shù)out本身的變量對象以及全局的變量對象。查找標識符時,先在函數(shù)out變量對象中尋找,找不到的話再去上一級全局變量對象查找。
// out 函數(shù)的執(zhí)行上下文
outEC = {
    VO: {
        arguments: {...},
        inner: <inner reference>,  // 表示 inner 的地址引用
        b: undefined
    },
    scopeChain: [VO(out), VO(global)], // 作用域鏈
}
函數(shù)out作用域鏈
  1. 函數(shù)inner的作用域鏈:可以引用函數(shù)inner本身的變量對象和上一級out函數(shù)的變量對象以及全局的變量對象。查找標識符時依次從inner,out,全局變量對象中查找。
innerEC = {
    VO: {
        arguments: {...},  
        c: undefined,
    },  // 變量對象
    scopeChain: [VO(inner), VO(out), VO(global)], // 作用域鏈
}
函數(shù)inner作用域鏈

(三)this指向

有關(guān)this的指向的詳情,可查看《【你不知道的JavaScript】(四)this的全面解析》。

三、執(zhí)行上下文棧

執(zhí)行上下文可以理解為當前代碼的執(zhí)行環(huán)境,JavaScript中的運行環(huán)境大概包括三種情況:

  • 全局環(huán)境JavaScript代碼運行起來會首先進入該環(huán)境
  • 函數(shù)環(huán)境:當函數(shù)被調(diào)用執(zhí)行時,會進入當前函數(shù)中執(zhí)行代碼
  • eval

在代碼開始執(zhí)行時,首先會產(chǎn)生一個全局執(zhí)行上下文環(huán)境,調(diào)用函數(shù)時,會產(chǎn)生函數(shù)執(zhí)行上下文環(huán)境,函數(shù)調(diào)用完成后,它的執(zhí)行上下文環(huán)境以及其中的數(shù)據(jù)都會被銷毀,重新回到全局執(zhí)行環(huán)境,網(wǎng)頁關(guān)閉后全局執(zhí)行環(huán)境也會銷毀。其實這是一個壓棧出棧的過程,全局上下文環(huán)境永遠在棧底,而當前正在執(zhí)行的函數(shù)上下文在棧頂

var a = 1;             //1.進入全局上下文環(huán)境
function out() {
    var b = 2;
    function inner() {
        var c = 3;
        console.log(a+b+c);
    }
    inner();          //3.進入inner函數(shù)上下文環(huán)境
}
out(); //2.進入out函數(shù)上下文環(huán)境

↑以上代碼的執(zhí)行會經(jīng)歷以下過程:

  1. 當代碼開始執(zhí)行時就創(chuàng)建全局執(zhí)行上下文環(huán)境,全局上下文入棧。
  2. 全局上下文入棧后,其中的代碼開始執(zhí)行,進行賦值、函數(shù)調(diào)用等操作,執(zhí)行到out()時,激活函數(shù)out創(chuàng)建自己的執(zhí)行上下文環(huán)境,out函數(shù)上下文入棧。
  3. out函數(shù)上下文入棧后,其中的代碼開始執(zhí)行,進行賦值、函數(shù)調(diào)用等操作,執(zhí)行到inner()時,激活函數(shù)inner創(chuàng)建自己的執(zhí)行上下文環(huán)境,inner函數(shù)上下文入棧
  4. inner函數(shù)上下文入棧后,其中的代碼開始執(zhí)行,進行賦值、函數(shù)調(diào)用、打印等操作,由于里面沒有可以生成其他執(zhí)行上下文的需要,所有代碼執(zhí)行完畢后,inner函數(shù)上下文出棧。
  5. inner函數(shù)上下文出棧,又回到了out函數(shù)執(zhí)行上下文環(huán)境,接著執(zhí)行out函數(shù)中后面剩下的代碼,由于后面沒有可以生成其他執(zhí)行上下文的需要,所有代碼執(zhí)行完畢后,out函數(shù)上下文出棧。
  6. out函數(shù)上下文出棧后,又回到了全局執(zhí)行上下文環(huán)境,直到瀏覽器窗口關(guān)閉,全局上下文出棧。
執(zhí)行上下文入棧出棧的全過程

我們可以得到一些結(jié)論:

  • 全局上下文在代碼開始執(zhí)行時就創(chuàng)建,只有唯一的一個,永遠在棧底,瀏覽器窗口關(guān)閉時出棧
  • 函數(shù)被調(diào)用的時候創(chuàng)建上下文環(huán)境。
  • 只有棧頂?shù)纳舷挛奶幱诨顒訝顟B(tài),執(zhí)行其中的代碼。
最后編輯于
?著作權(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)容