講清楚之執行上下文

講清楚之執行上下文

標籤 : javascriptjavascript


什麼是執行上下文?

當 JavaScript 代碼執行一段可執行代碼時,會建立對應的上下文(execution context)並將該上下文壓入上下文棧(context stack)中。java

上下文包含如下3個重要屬性:node

name -
變量對象(VO, variable object) 當前函數定義的變量、函數、參數
做用域鏈(Scope chain) 源代碼定義時造成的做用域鏈
this

上下文是一個抽象概念,爲了便於理解咱們假設上下文是一個對象而且包含VO、Scope、this這三個屬性:數組

function foo (c) {
  let a = 1
  let b = function () {}
}

// foo函數的上下文
fooContext = {
        VO: {
            arguments: { // 實參
              c: undefind,
              length: 0
            },
            a: 1, // 變量
            b: reference to function (){} // 函數
        },
        Scope: [VO, globalContext.VO], // 做用域鏈
        this: undefind // 非嚴格模式下爲 this
    }

因此上下文是函數運行時的環境或者說是依賴資源的集合,它決定了函數運行時能夠獲取到哪些變量、函數。瀏覽器

執行上下文(EC): 若是函數處於正在執行狀態則該函數的上下文稱爲執行上下文, 與此同時若是函數處於非執行狀態則爲(普通)上下文。因此執行上下文只是上下文的不一樣狀態,本質上它們沒有區別。函數

上下文棧

上下文棧又稱爲執行棧(ECS), 瀏覽器中 javascript 解析器自己是單線程的,即同一時間只能處理一個上下文及對應的代碼段,因此 javascript 解析引擎使用上下文棧來管理上下文。全部的上下文建立後會保存在上下文棧隊列裏。棧底爲全局上下文,棧頂爲當前正在執行的上下文。this

圖片描述

一個上下文就是一個執行單元, javascript 以棧的方式管理執行單元。頁面初始化的時候首先會在棧底壓入全局上下文,而後根據規則執行到可執行函數時會將函數的上下文壓入上下文棧 中, 被壓入的上下文包含有該函數運行時所需的資源(變量對象、做用域鏈、this),這些資源提供給函數運行時的表達式使用。spa

執行上下文能夠理解爲函數運行時的環境。同時執行上下文也是一個不可見的概念。線程

javascript 中有3種運行環境:code

  • 全局環境: 在瀏覽器中是window, 在 node 環境中是global,當頁面初始化時會將全局上下文壓入上下文棧;
  • 函數環境: 當函數被調用執行時會收集該函數的資源,建立上下文並壓入上下文棧;
  • eval環境,棄用

一個運行環境會對應一個上下文。位於棧頂的上下文執行完畢後會自動出棧,依次向下直至全部上下文運行完畢,最後瀏覽器關閉時全局上下文被銷燬。爲了好理解來舉個栗子:

let i = 0
function foo () {
    i++
    console.log(i, 'foo')
}
function too () {
    i++
    console.log(i, 'too')
    foo()
}
function don () {
    i++
    console.log(i, 'don')
    too()
}
don()

 // 1 "don"
 // 2 "too"
 // 3 "foo"

上面代碼的邏輯就是先執行don(),而後是too()、foo()。執行到foo()時的上下文棧是這樣的:

圖片描述

咱們假設上下文棧爲一個數組:ECStack

ECStack = []

javascript 載入完成後首先解析執行的是全局代碼,因此初始化的時候會向上下文棧中 push 全局上下文,咱們用globalContext來表示。

ECStack = [
    globalContext
]

全局做用域在整個代碼運行階段會一直存在,直至頁面關閉時 ECStack 會被請空,從而globalContext則被銷燬。

全局上下文建立的時候進行變量提高、生成變量對象等操做,然後會執行當前上下文中的可執行代碼(函數、表達式)。遇到函數調用的時候會向上下文棧中push該函數的上下文。

function foo () {
    console.log('foo')
}
function too () {
    console.log('too')
    foo()
}
function don () {
    too()
}
don()

執行邏輯能夠理解爲:

  1. 執行到 don(), 解析 don函數內部代碼
  2. 生成 don 函數的上下文(vo、Scope chain、this)
  3. 壓入 don 的上下文到 ECStack
  4. 執行 don 函數體內部的表達式
  5. 執行 too()
  6. 生成 too 函數的上下文(vo、Scope chain、this)
  7. 壓入 too 的上下文到 ECStack
  8. ...

javascript 解析器不斷遞歸直到 foo 函數執行完...foo 函數上下文被彈出...而後回溯到globalContext上下文...等待...當事件的回調函數被激活後,執行回調函數。( 這裏涉及到 javascript 的執行機制和事件循環,請關注後續文章^_^)

執行邏輯的僞代碼以下:

// 僞代碼

// don()
ECStack.push(<don> functionContext);

// 在don中調用了too, push too的上下文到上下文棧裏
ECStack.push(<fun2> functionContext);

// 在too中調用了foo, push foo的上下文到上下文棧裏
ECStack.push(<fun3> functionContext);

// foo執行完畢, 彈出上下文
ECStack.pop();

// too執行完畢, 彈出上下文
ECStack.pop();

// don執行完畢, 彈出上下文
ECStack.pop();

// 非全局上下文執行完畢被彈出後會一直停留在全局上下文裏,直至頁面關閉
須要注意的是,上下文與做用域(scope)是不一樣的概念。上下文是一個運行時概念,瀏覽器運行後執行 js 代碼,將不一樣的上下文加入上下文棧中,頂層的上下文對應的代碼塊執行完後又將該上下文銷燬。 而做用域是一個靜態概念,根據所在代碼片斷的位置及詞法關係確立的,無論瀏覽器運行與否,源代碼的做用域關係、變量的訪問權限依然不變。
相關文章
相關標籤/搜索