JavaScript中的執行上下文和變量對象

執行上下文(Execution Context)

文章同步到github javaScript中的執行上下文和變量對象html

JavaScript代碼執行的過程,包括編譯和執行兩個階段,編譯就是經過詞法分析,構建抽象抽象語法樹,並編譯成機器識別的指令,在JavaScript代碼編譯階段,做用域規則就已經肯定了;在代碼執行階段,或者函數一旦調用,便會建立執行上下文(Execution Context),也叫執行環境前端

在ECMA-262中有以下一段定義java

當控制器轉入 ECMA 腳本的可執行代碼時,控制器會進入一個執行環境。當前活動的多個執行環境在邏輯上造成一個棧結構。該邏輯棧的最頂層的執行環境稱爲當前運行的執行環境。任什麼時候候,當控制器從當前運行的執行環境相關的可執行代碼轉入與該執行環境無關的可執行代碼時,會建立一個新的執行環境。新建的這個執行環境會推入棧中,成爲當前運行的執行環境.

這也是一個抽象的概念,在一段JavaScript代碼中,會建立多個執行上下文,執行上下文定義了變量或函數有權訪問的其餘數據, ,經過閱讀規範及相關文檔,瞭解到執行上下文(簡稱EC)主要包括三個點,用僞代碼表示以下:git

EC = {
    this: // 綁定this指向爲當前執行上下文, 若是函數屬於全局函數,則this指向window
    scopeChain: [] // 建立當前執行環境的做用域鏈,
    VO: {} // 當前環境的變量對象(Variable Object),每一個環境都有一個與之關聯的變量對象
}

看下面這一段代碼:github

var a = 1;
function foo() {
    var b = 2;
    function bar() {
        console.log(b)
    }
    bar()
    console.log(a);
}

foo()
  • 1.執行這段代碼,首先會建立全局上下文globleEC,並推入執行上下文棧中;
  • 2.當調用foo()時便會建立foo的上下文fooEC,並推入執行上下文棧中;
  • 3.當調用bar()時便會建立bar的上下文barEC,並推入執行上下文棧中;
  • 4.當bar函數執行完,barEC便會從執行上下文棧中彈出;
  • 5.當foo函數執行完,fooEC便會從執行上下文棧中彈出;
  • 6.在瀏覽器窗口關閉後,全局上下文globleEC便會從執行上下文棧中彈出;

總結: 棧底永遠都是全局上下文,而棧頂就是當前正在執行的上下文數組

再舉一個例子結合瀏覽器開發者工具來看看到底什麼執行上線文瀏覽器

function foo() {
    bar()
    console.log('foo')
}

function bar() {
    baz()
    console.log('bar')
}

function baz() {
    debugger  // 打斷點觀察執行上下文棧中的狀況
}

能夠看到當前baz正在執行,因此棧頂是baz的執行上下文,而棧底永遠都是Global上下文ecmascript

callStack

繼續執行,baz函數執行完成後,從執行上下文棧頂彈出,繼續執行bar函數內後面的代碼,bar函數執行完,bar的執行上下文從棧中彈出;而後執行foo函數後面的代碼,foo函數執行完後,從執行上下文從棧中彈出;最後全局上下文從執行上下文從棧中彈出,清空執行上下文從棧。函數

clearCallStack

變量對象(Variable Object):

每個執行環境都有一個與之關聯的變量對象,是一個抽象的概念,環境中定義的全部變量和函數都保存在這個對象中。雖然咱們編寫的代碼沒法訪問這個對象,但解析器在處理數據時會在後臺使用它們。

當瀏覽器第一次加載js腳本程序的時候, 默認進入全局執行環境, 這次的全局環境變量對象爲window, 在代碼中能夠訪問。工具

若是環境是函數, 則將此活動對象作爲當前上下文的變量對象(VO = AO), 此時變量對象是不可經過代碼來訪問的,下面主要對活動對象進行講解。

活動對象(Activation Object)

1.初始化活動對象(下文縮寫爲AO)

當函數一調用,馬上建立當前上下文的活動對象, 並將活動對象做爲變量對象,經過arguments屬性初始化,值爲arguments對象(傳入的實參集合,與形參無關,形參作爲局部環境的局部變量被定義)

AO = {
  arguments: <ArgO>
};

arguments對象有如下屬性:

  • length: 真正傳遞參數的個數;
  • callee: 指向當前函數的引用,也就是被調用的函數;
  • '類index': 字符串類型的整數, 值就是arguments對象中對象下標的值,arguments對象應和數組加以區別, 它就是arguments對象,只是能和數組具備相同的length屬性,和能夠經過下標來訪問值
function show (a, b, c) {
    // 經過Object.prototype.toString.call()精準判斷類型, 證實arguments不一樣於數組類型
    var arr = [1, 2, 3];
    console.log(Object.prototype.toString.call(arr)); // [object Array]

    console.log(Object.prototype.toString.call(arguments)); // [object Arguments]

    console.log(arguments.length) // 2  傳遞進來實參的個數

    console.log(arguments.callee === show) // true 就是被調用的函數show自身

    //參數共享

    console.log(a === arguments[0]) // true

    a = 15;

    console.log(arguments[0]) // 15

    arguments[0] = 25;

    console.log(a)  // 25;

    可是,對於沒有傳進來的參數c, 和arguments的第三個索引是不共享的

    c = 25;

    console.log(arguments[2]) // undefined

    argument[2] = 35;

    console.log(c) // 25

}

show(10, 20);

接着往下走,這纔是關鍵的地方,執行環境的代碼被分紅兩個階段來處理:

  1. 進入執行環境
  2. 執行函數的代碼

2.進入執行環境

函數若是被調用, 進入執行環境(上下文),並當即建立活動對象, 經過arguments屬性初始化, 與此同時會掃描執行環境中的全部形參、全部函數聲明、全部變量聲明, 添加到活動對象(AO)中, 並肯定this的值,而後會開始執行代碼。

在進入執行環境這個階段:

全部形參聲明:

形參名稱做爲活動對象屬性被建立, 若是傳遞實參, 值就爲實參值, 若是沒有傳遞參數, 值就爲undefined

全部函數聲明:

函數名稱做爲活動對象的屬性被建立,值是一個指針在內存中, 指向這個函數,若是變量對象已經存在相同名稱的屬性, 則徹底替換。

全部變量聲明:

全部變量名稱做爲活動對象的屬性被建立, 值爲undefined,可是和函數聲明不一樣的是, 若是變量名稱跟已經存在的屬性(形式參數和函數)相同、則不會覆蓋
function foo(a, b) {
    var c = 10;
    function d() {
        console.log('d');
    }
    var e = function () {
        console.log('e');
    };
    (function f() {})
    if (true) {
        var g = 20;
    } else {
        var h = 30;
    }
}

foo(10);

此時在進入foo函數執行上下文時,foo的活動對象fooAO爲:

fooAO = {
    arguments: {
        0: 10,
        length: 1
    },
    a: 10,
    b: undefined,
    c: undefined,
    d: <d reference>  //指向d函數的指針,
    e: undefined,
    g: undefined,
    h: undefined  // 雖然else中的代碼永遠不會執行,可是h仍然是活動對象中的屬性
}

這個例子作以下幾點說明:

  • 1.關於函數,只會建立函數聲明做爲活動對象的屬性, 而f函數做爲函數表達式並不會出如今活動對象(AO)中
  • 2.e雖然值是一個函數, 可是做爲變量屬性被活動對象建立

3.代碼執行階段

在進入執行上下文階段,活動對象擁有了屬性,可是不少屬性值爲undefined, 到代碼執行階段就開始爲這些屬性賦值了

仍是上面的代碼例子, 此時活動對象以下:

fooAO = {
    arguments: {
        0: 10,
        length: 1
    },
    a: 10,
    b: undefined,
    c: 10, // 賦值爲undefined
    d: <d reference>  //指向d函數的指針,
    e: <d reference>  // 指向e函數的指針
    g: 20,
    h: undefined  // 聲明h變量,可是沒有賦值
}

變量對象包括:{ arguments對象+函數形參+內部變量+函數聲明(但不包含表達式) }

這時這個活動對象, 即做爲當前執行環境的變量對象會被推到此執行環境做用域鏈的最前端(做用域鏈本篇不作介紹,會在下一篇文章中單獨講解做用域和做用域鏈), 假定執行環境爲一個對象,則整個執行環境能夠訪問到的屬性以下:

僞代碼以下:

fooExecutionContext = {
    scopeChain: [], //fooAO +全部父執行環境的活動對象,
    fooAO: {
        arguments: {
            0: 10,
            length: 1
        },
        a: 10,
        b: undefined,
        c: 10, // 賦值爲undefined
        d: <d reference>  //指向d函數的指針,
        e: <d reference>  // 指向e函數的指針
        g: 20,
        h: undefined
    },
    this: 當前執行環境的上下文指針
}

補充:

下面的例子爲了說明一下變量聲明的順序及變量同名不會影響函數聲明

console.log(foo); //  foo的函數體
var foo = 10;
console.log(foo) // 10
function foo() {};
foo = 20;
console.log(foo); // 20

在代碼執行以前, 就會讀取函數聲明,變量聲明的順序在函數聲明和形參聲明以後, 整個流程以下:

1. 進入執行環境階段:

1. var VO = {}
2. VO[foo] = 'foo函數指針'
3. 掃描到var foo = 10,

 // 可是foo作爲function已經聲明,因此變量聲明不會影響同名的函數聲明,若是代碼中沒有foo函數聲明的話,則foo爲undefined

代碼執行階段:

1. VO[foo] = 10;
2. VO[foo] = 20;

解析代碼完成。

以上簡單總結了下對執行上下文和變量對象的理解,主要在於記錄總結一下學習成果,目前文章的水平實在不敢談分享。若有理解不對的地方還請各位大神多多指教,想了解更深能夠去查看本文最後主要參考資料的連接,都是經典啊,相信看完也就理解了。

本文主要參考資料:

JavaScript高級程序設計(第3版)
ECMAScript5.1中文版--執行環境
前端基礎進階(二):執行上下文詳細圖解
前端基礎進階(三):變量對象詳解
深刻理解JavaScript系列(11):執行上下文(Execution Contexts)
深刻理解JavaScript系列(12):變量對象(Variable Object)

相關文章
相關標籤/搜索