JavaScript核心之執行上下文、做用域鏈、閉包

執行上下文EC(Execution Context)

  1. 執行上下文就是一個函數或者全局代碼運行時的環境,裏面包括執行時須要用到的數據,能夠理解爲是一個對象;
  2. 函數每次執行都會建立一個新的執行上下文;
  3. 函數每次執行完畢都會銷燬這個執行上下文;

執行棧CS(call stack)

  1. 每次有函數執行就會生成一個新的執行上下文,而組織管理執行上下文的棧就是執行棧;
  2. 當一個函數運行時,會在運行前生成一個執行上下文併入棧,函數運行結束就會出棧;
  3. 每次正在運行的函數使用的都是棧頂的上下文;
  4. 棧底永遠是全局上下文。

舉個例子:數組

function foo() {
    console.log('foo函數執行了');

    function bar() {
        console.log('bar函數執行了');
    }
    
    bar();
}

foo();

代碼執行時,執行棧如何變化緩存

  1. 建立全局上下文G_EC,併入棧。閉包

    執行棧:app

    G_EC
  2. 執行foo函數,建立foo的執行上下文foo_EC, 併入棧。模塊化

    執行棧:函數

    foo_EC

    G_ECthis

  3. 執行console.log函數,建立console.log函數的執行上下文clg_EC, 併入棧。spa

    執行棧:code

    clg_EC

    foo_EC模塊化開發

    G_EC

  4. console.log執行完畢,打印出 'foo函數執行了',銷燬clg_EC,出棧。

    執行棧:

    foo_EC

    G_EC

  5. 執行bar函數,建立bar的執行上下文bar_EC, 併入棧。

    執行棧:

    bar_EC

    foo_EC

    G_EC

  6. 執行console.log函數,建立console.log函數的執行上下文clg_EC, 併入棧。

    執行棧:

    clg_EC

    bar_EC

    foo_EC

    G_EC

    注意:這裏的clg_EC是一個全新的上下文,和上一個不同,函數每次調用都生成一個新的獨一無二的執行上下文。

  7. console.log執行完畢,打印出 'bar函數執行了',銷燬clg_EC,出棧。

    執行棧:

    bar_EC

    foo_EC

    G_EC

  8. bar函數執行完畢,銷燬bar_EC, 出棧。

    執行棧:

    foo_EC

    G_EC

  9. foo函數執行完畢,銷燬foo_EC, 出棧。

    執行棧:

    G_EC
  10. 所有代碼執行完畢,銷燬G_EC, 出棧。

    執行棧:

執行上下文中的內容

//用代碼表示一個執行上下文
EC = {
    VO = {...},
    SC = [...],
    this = {...}
}

1. 變量對象VO(Varibale Object)

  1. 函數運行前一刻生產;
  2. 執行棧頂部的VO,又被稱之爲執行對象AO(Active Object)
  3. 執行棧底部的VO,又被稱之爲全局對象GO(Global Object)

2. 做用域鏈SC(Scope Chain)

  1. 做用域鏈(Scope Chain)是一個數組,是執行上下文的集合,每一個函數都有一個[[scope]]屬性,指向一個數組,數組中保存的是除本身的執行上下文之外的其餘執行上下文;
  2. 做用域鏈是在函數定義時就產生的;
  3. 當一個函數要使用一個變量時,會先從本身的執行上下文中查找,若是找不到,就會沿着做用域鏈往上找。

3. this

不影響執行做上下文的理解,可跳過。

this的指向問題:

  1. 全局環境中,this指向window對象

    console.log(this); //window
  2. 函數中的this,指向window(嚴格模式中指向undefined

    function foo(){
        console.log(this); //window
    }
    
    foo();
  3. 使用callapply調用,this指向call/apply的第一個參數

    var obj = {a: 1};
    
    function foo(){
        console.log(this); //obj
    }
    
    foo.call(obj);
    foo.apply(obj);
  4. 調用對象中的函數,使用obj.fun方式調用,this指向obj

    var obj = {
        a: 1,
        foo: function(){
            console.log(this);
        }
    };
    
    obj.foo(); //this指向obj
    
    var bar = obj.foo;
    bar(); //至關於將函數放在全局中執行,this指向window

VO的建立過程

在一個函數的VO建立時,js引擎作的事情

  1. 肯定形參的值
  2. 變量的聲明提高
  3. 將實參的值賦給形參
  4. 函數聲明總體提高
遇到同名屬性則覆蓋

舉個例子:

function foo(a, b){
    console.log(bar); //ƒ bar(){}
    console.log(a); //2

    function bar(){}
    
    var a = 1;
}

foo(2, 3);

按照建立步驟生成foo函數的VO——foo_VO

//1. 肯定形參的值,一開始有兩個形參的值
foo_VO = {
    arguments: {...}, //arguments一開始就會在
    a: undefined,
    b: undefined,
}

//2. 變量聲明提高, 內部有一個變量聲明a,當前VO對象已經有a屬性,因此不變
foo_VO = {
    arguments: {...},
    a: undefined,
    b: undefined,
}

//3. 將實參的值賦給形參,執行foo(2, 3)時傳入了實參2, 3,分別賦值給a, b
foo_VO = {
    arguments: {...},
    a: 2,
    b: 3,
}

//4. 函數聲明提高, 有一個函數foo,將foo函數做爲VO的屬性
foo_VO = {
    arguments: {...},
    a: 2,
    b: 3,
    foo: function(){}
}

因此最後foo函數產生的VO對象就是

foo_VO = {
    arguments: {...},
    a: 2,
    b: 3,
    foo: function(){}
}
由於VO是在函數運行前建立的,函數在運行的時候就能夠在當前VO中查找變量,因此這就解釋了爲何 console.log放在函數的最前面也能夠打印 afoo的值。

SC的內容

SC = 上一層執行上下文棧的AO + 上一層執行上下文棧的SC

舉個例子:

function foo() {
    function bar() {
        function baz(){
            
        }
    }
}

全局執行上下文,一開始的SC爲空

g_EC = {
    SC: [],
    AO: {...},
    this: {...},
}

foo函數執行上下文,其中SC = 全局上下文的AO + 全局上下文的SC

foo_EC = {
    SC: [g_EC.AO], //[g_EC.AO, ...g_EC.SC]
    AO: {...},
    this: {...},
}

bar函數執行上下文,其中SC = foo函數執行上下文的AO + foo函數執行上下文的SC

bar_EC = {
    SC: [foo_SC.AO, g_EC.AO], //[foo_SC.AO, ...foo_SC.SC],
    AO: {...},
    this: {...},
}

baz函數執行上下文,其中SC = bar函數執行上下文的AO + bar函數執行上下文的SC

baz_EC = {
    SC: [bar_SC.AO, foo_SC.AO, g_EC.AO], //[bar_SC.AO, ...bar_SC.SC],
    AO: {...},
    this: {...},
}

做用域圖解:

做用域圖解

函數運行時,查找變量,會先查找本身的AO。若是沒有,再依次沿着SC的第0項、第1項... 日後找。找到SC的最後一項都沒有找到就會報錯: Uncaught ReferenceError: xxx is not defined

閉包

當一個函數中的函數被保存到該函數的外部就會造成閉包。

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

var baz = foo();
var qux = baz();

console.log(qux); //1

foo在運行的時候

foo_EC = {
    SC: [GO],
    AO: {
        a: 1,
        ...
    },
    this: {...},
}

foo運行完畢後會銷燬本身的執行上下文,其中的AO也被銷燬

可是因爲bar被保存到了外部, 也就是baz中,而bar的做用域SC中有foo的AO,因此這就解釋了爲何造成閉包時,外部的函數能夠使用函數內部的變量。

baz_EC = {
    SC: [foo_EC.AO, GO],
    AO: {...},
    this: {...},
}
由於 bar函數被保存到全局做用域中,其中的 foo的AO一直存在,沒法被銷燬,會形成內存泄露;

在使用完畢後應該去掉原來的閉包baz = null

閉包的做用:

  1. 實現公有變量;
  2. 能夠作緩存;
  3. 實現屬性私有化;
  4. 模塊化開發,防止污染全局變量。
相關文章
相關標籤/搜索