JavaScript進(jìn)階-執(zhí)行上下文(理解執(zhí)行上下文一篇就夠了)

前言

在編程這個行業(yè)中總是能聽到這個詞執(zhí)行上下文。那么什么叫執(zhí)行上下文呢?

本篇文章主要是介紹javascript中的執(zhí)行上下文, 看完之后你可以了解到:

  • 執(zhí)行上下文的類型
  • 執(zhí)行上下文特點(diǎn)
  • 執(zhí)行棧
  • 執(zhí)行上下文的生命周期

概念

首先我們來介紹什么是“執(zhí)行上下文”.

舉個例子,生活中,相同的話在不同的場合說可能會有不同的意思,而這個說話的場合就是我們說話的語境。

同樣對應(yīng)在編程中, 對程序語言進(jìn)行“解讀”的時候,也必須在特定的語境中,這個語境就是javascript中的執(zhí)行上下文。

一句話概括:

執(zhí)行上下文就是javascript代碼被解析和執(zhí)行時所在環(huán)境的抽象概念。

執(zhí)行上下文的類型

js中,執(zhí)行上下文分為以下三種:

  • 全局執(zhí)行上下文:只有一個,也就是瀏覽器對象(即window對象),this指向的就是這個全局對象。
  • 函數(shù)執(zhí)行上下文:有無數(shù)個,只有在函數(shù)被調(diào)用時才會被創(chuàng)建,每次調(diào)用函數(shù)都會創(chuàng)建一個新的執(zhí)行上下文。
  • Eval函數(shù)執(zhí)行上下文jseval函數(shù)執(zhí)行其內(nèi)部的代碼會創(chuàng)建屬于自己的執(zhí)行上下文, 很少用而且不建議使用。

執(zhí)行上下文的特點(diǎn)

  1. 單線程,只在主線程上運(yùn)行;
  2. 同步執(zhí)行,從上向下按順序執(zhí)行;
  3. 全局上下文只有一個,也就是window對象;
  4. 函數(shù)執(zhí)行上下文沒有限制;
  5. 函數(shù)每調(diào)用一次就會產(chǎn)生一個新的執(zhí)行上下文環(huán)境。

JS如何管理多個執(zhí)行上下文

通過上面介紹,我們知道了js代碼在運(yùn)行時可能會產(chǎn)生無數(shù)個執(zhí)行上下文,那么它是如何管理這些執(zhí)行上下文的呢?

同時由于js是單線程的,所以不能同時干兩件事,必須一個個去執(zhí)行,那么這么多的執(zhí)行上下文是按什么順序執(zhí)行的呢?

執(zhí)行棧

接下來就對上面的問題做出解答,管理多個執(zhí)行上下文靠的就是執(zhí)行棧,也被叫做調(diào)用棧。

特點(diǎn):后進(jìn)先出(LIFO)的結(jié)構(gòu)。

作用:存儲在代碼執(zhí)行期間的所有執(zhí)行上下文。

LIFO: last-in, first-out,類似于向乒乓球桶中放球,最先放入的球最后取出)

js在首次執(zhí)行的時候,會創(chuàng)建一個全局執(zhí)行上下文并推入棧中。

每當(dāng)有函數(shù)被調(diào)用時,引擎都會為該函數(shù)創(chuàng)建一個新的函數(shù)執(zhí)行上下文然后推入棧中。

當(dāng)棧頂?shù)暮瘮?shù)執(zhí)行完畢之后,該函數(shù)對應(yīng)的執(zhí)行上下文就會從執(zhí)行棧中pop出,然后上下文控制權(quán)移到下一個執(zhí)行上下文。

比如下面的一個例子??:

var a = 1; // 1. 全局上下文環(huán)境
function bar (x) {
    console.log('bar')
    var b = 2;
    fn(x + b); // 3. fn上下文環(huán)境
}
function fn (c) {
    console.log(c);
}
bar(3); // 2. bar上下文環(huán)境

如下圖:

context1

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

執(zhí)行上下文的生命周期也非常容易理解, 分為三個階段:

  1. 創(chuàng)建階段
  2. 執(zhí)行階段
  3. 銷毀階段

創(chuàng)建階段

創(chuàng)建階段, 主要有是有這么幾件事:

  1. 確定this的值, 也就是綁定this (This Binding);
  2. 詞法環(huán)境(LexicalEnvironment)組件被創(chuàng)建;
  3. 變量環(huán)境(VariableEnvironment)組件被創(chuàng)建.

一張圖方便你理解 ??

executionContext1

有一些教材中也喜歡用偽代碼來實(shí)現(xiàn):

ExecutionContext = {  
  ThisBinding = <this value>,     // 確定this 
  LexicalEnvironment = { ... },   // 詞法環(huán)境
  VariableEnvironment = { ... },  // 變量環(huán)境
}

This Binding

通過上面的介紹我們知道實(shí)際開發(fā)主要用到兩種執(zhí)行上下文為全局函數(shù), 那么綁定this在這兩種上下文中也不同.

  • 全局執(zhí)行上下文中, this指的就是全局對象, 瀏覽器環(huán)境指向window對象, nodejs中指向這個文件的module對象.
  • 函數(shù)執(zhí)行上下文較為復(fù)雜, this的值取決于函數(shù)的調(diào)用方式. 具體有: 默認(rèn)綁定、隱式綁定、顯式綁定、new綁定、箭頭函數(shù).

詞法環(huán)境

如上圖, 詞法環(huán)境是由兩個部分組成的:

  1. 環(huán)境記錄: 存儲變量和函數(shù)聲明的實(shí)際位置;
  2. 對外部環(huán)境的引用: 用于訪問其外部詞法環(huán)境.

同樣的, 詞法環(huán)境也主要有兩種類型:

  1. 全局環(huán)境: 擁有一個全局對象(window對象)及其關(guān)聯(lián)的所有屬性和方法(比如數(shù)組的方法splice、concat等), 同時也包含了用戶自定義的全局變量. 但是全局環(huán)境中沒有外部環(huán)境的引用, 也就是外部環(huán)境引用為null.
  2. 函數(shù)環(huán)境: 用戶在函數(shù)中自定義的變量和函數(shù)存儲在環(huán)境記錄中, 包含了arguments對象. 而對外部環(huán)境的引用可以是全局環(huán)境, 也可以是另一個函數(shù)環(huán)境(比如一個函數(shù)中包含了另一個函數(shù)).

繼續(xù)用偽代碼來實(shí)現(xiàn):

GlobalExectionContext = { // 全局執(zhí)行上下文
    LexicalEnvironment: {   // 詞法環(huán)境
        EnvironmentRecord: {   // 環(huán)境記錄
            Type: "Object"       // 全局環(huán)境
            // 標(biāo)識符綁定在這里
        },
        outer: <null>          // 外部環(huán)境引用
    }
}
FunctionExectionContext = { // 函數(shù)執(zhí)行上下文
    LexicalEnvironment: {   // 詞法環(huán)境
        EnvironmentRecord: {   // 環(huán)境記錄
            Type: "Object",       // 函數(shù)環(huán)境
            // 標(biāo)識符綁定在這里
        },
    outer: < Global or FunctionEnvironment> // 外部環(huán)境引用
    }
}

變量環(huán)境

變量環(huán)境其實(shí)也是一個詞法環(huán)境, 因此它具有上面定義的詞法環(huán)境的所有屬性.

在 ES6 中,詞法 環(huán)境和 變量 環(huán)境的區(qū)別在于前者用于存儲函數(shù)聲明和變量( letconst綁定,而后者僅用于存儲變量( var綁定。

案例??:

var a;
var b = 1;
let c = 2;
const d = 3;
function fn (e, f) {
    var g = 4;
    return e + f + g;
}
a = fn(10, 20);

執(zhí)行上下文如下:

GlobalExectionContext = { // 全局執(zhí)行上下文
    ThisBinding: <Global Object>,
    LexicalEnvironment: {   // 詞法環(huán)境
        EnvironmentRecord: {   // 環(huán)境記錄
            Type: "Object",       // 全局環(huán)境
            c: < uninitialized >,
                d: < uninitialized >,
            fn: < func >
        },
        outer: <null>            // 外部環(huán)境引用
    },
    VariableEnvironment: {   // 變量環(huán)境
        EnvironmentRecord: {   // 環(huán)境記錄
            Type: "Object",
            a: < uninitialized >,
            b: < uninitialized >
        },
        outer: <null>  
    }
}
FunctionExectionContext = { // 函數(shù)執(zhí)行上下文
    ThisBinding: <Global Object>, // this綁定window, 因?yàn)檎{(diào)用fn的是window對象
    LexicalEnvironment: {   // 詞法環(huán)境
        EnvironmentRecord: {   // 環(huán)境記錄
            Type: "Object",       // 函數(shù)環(huán)境
            Arguments: { 0: 10, 1: 20, length: 2 }
        },
        outer: < GlobalLexicalEnvironment > // 全局環(huán)境的引用
    },
    VariableEnvironment: {   // 變量環(huán)境
        EnvironmentRecord: {   // 環(huán)境記錄
            Type: "Object",
            g: < uninitialized >
        },
        outer: < GlobalLexicalEnvironment > // 全局環(huán)境的引用
    }
}

因此我們可以知道變量提升的原因是:

在創(chuàng)建階段,函數(shù)聲明存儲在環(huán)境中,而變量會被設(shè)置為 undefined(在 var 的情況下)或保持未初始化(在 letconst 的情況下)。所以這就是為什么可以在聲明之前訪問 var 定義的變量(盡管是 undefined ),但如果在聲明之前訪問 letconst 定義的變量就會提示引用錯誤的原因。這就是所謂的變量提升。

執(zhí)行階段

執(zhí)行階段主要做三件事情:

  1. 變量賦值
  2. 函數(shù)引用
  3. 執(zhí)行其他的代碼

注??

如果 Javascript 引擎在源代碼中聲明的實(shí)際位置找不到 let 變量的值,那么將為其分配 undefined 值。

銷毀階段

執(zhí)行完畢出棧,等待回收被銷毀

后語

該篇文章僅僅只是對執(zhí)行上下文做一個入門程度的介紹, 后面會深入介紹它.

參考文章:

木易楊前端進(jìn)階-理解JavaScript 中的執(zhí)行上下文和執(zhí)行棧

JS執(zhí)行上下文

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

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

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