javascript之作用域與作用域鏈

minghu發表於2018-11-09

前言

作用域是指程式中能定義變數或者函式的範圍,可以理解為地盤,是一個抽象的概念。
作用域分為靜態作用域和動態作用域,在JavaScript中採用的是靜態作用域,也稱詞法作用域。

正文

靜態作用域

靜態作用域是指宣告的作用域在程式被定義的時候就確定的,在JavaScript中,當函式定義的時候,函式作用域就被建立了,此時的作用域就是靜態作用域。

看如下程式碼:

    'use strict';
    
    var a = 1;
    
    function fn1() {
        console.log(a);
    };
    
    function fn2() {
        var a = 2;
        fn1(); // 1
    };
    
    fn2();
複製程式碼

當函式fn1被呼叫的時候,fn1函式內部會去找變數a,先從函式內部開始找,如果沒找到,則從定義fn1函式的上一層作用域找,結果找到了a,值為1。所以上面的程式碼執行結果是會在控制檯列印1。

作用域鏈

首先需要知道幾個概念:

每個函式內部都有一個[[scope]]的屬性(這個屬性僅供js引擎使用),當函式被建立的時候,[[scope]]屬性會儲存建立該函式的作用域中的所有物件(Variable Obejct)。

每個函式被呼叫的時候,會產生一個執行上下文的內部物件,這個物件裡面包含了:

  • 作用域鏈([[scope]])
  • 活動物件(Active Object)
  • this

說明: 執行上下文在函式執行完畢之後,會自動銷燬,他裡面的作用域鏈是複製的函式內部的屬性[[scope]]。

通過下面程式碼的執行過程,來深入理解作用域鏈:

    'use strict';
    
    var a = 1;
    function fn1(x) {
        var b = 2;
        return (a + b + x);
    };
    fn1(1);
複製程式碼

執行過程如下:
1、建立全域性執行上下文環境(Global Execution context,下面簡稱globalEC),全域性執行上下文壓入執行上下文棧(Execution context stack,下面簡稱ECStack)。

    // 虛擬碼
    
    //建立全域性執行上下文環境
    globalEC = {}
    
    //全域性執行上下文壓入執行棧中
    ECStack = [
        globalEC
    ]
複製程式碼

2、全域性執行上下文環境初始化,即預解析階段,會進行函式提升與變數提升

    // 虛擬碼
    
    //全域性上下文環境初始化,GO代表的是全域性作用域中儲存的變數
    globalEC = {
        this: [global],
        GO: {
            [global]: [global],
            this: [global],
            a: undefined,
            fn1: function() {//...}
        }
    }
    
    // 函式fn1被建立時,fn1內部的[[scope]]被賦值
    fn1.[[scope]] = [globalEC.GO]
複製程式碼

3、進入執行階段,程式碼按預解析後的順序執行,並給變數賦值

    // 虛擬碼,邊執行邊給變數賦值
    globalEC = {
        this: [global],
        GO: {
            [global]: [global],
            this: [global],
            a: 1,
            fn1: function() {//...}
        }
    }
    
複製程式碼

4、當執行fn1()時,建立fn1的執行上下文(fn1 Execution context,下面簡稱fn1EC),fn1函式的執行上下文壓入棧中,處於活動狀態

    // 虛擬碼
    
    //建立fn1的執行上下文
    fn1EC = {}
    
    //fn1函式的執行上下文壓棧
    ECStack = [
        fn1Ec,
        globalEC
    ]
複製程式碼

5、fn1函式進入預編譯階段,產生活動物件AO,活動物件AO被儲存到作用域鏈的頂端,

    // 虛擬碼
    
    //fn1函式進入預編譯階段,產生活動物件(Active object,下面簡稱AO)
    fn1EC = {
        this: undefined, // 在嚴格模式下為undefined,非嚴格模式下為全域性物件
        AO: {
            arguments: {
                0: undefined,
                length: 1
            },
            b: undefined
        },
        [[scope]]: [fn1EC.AO, globalEC.GO]
    }
複製程式碼

6、當fn1函式進入執行階段

    // 虛擬碼
    fn1EC = {
        this: undefined, // 在嚴格模式下為undefined,非嚴格模式下為全域性物件
        AO: {
            arguments: {
                0: 1,
                length: 1
            },
            b: 2
        },
        [[scope]]: [fn1EC.AO, globalEC.GO]
    }
複製程式碼

7、fn1函式內部引用了變數a, 但是內部並沒有定義,此時就會沿著從 AO -> GO 這條作用域鏈去找,於是在全域性作用域 GO 中找到了a的值。如果一直到全域性作用域沒有找到,那麼此時就會報錯了。
8、當fn1函式執行完畢,fn1的執行上下文出棧,並銷燬,此時回到全域性執行上下文環境。

    // 虛擬碼,fn1函式的執行上下文出棧,此時回到全域性執行上下文環境
    ECStack = [
        globalEC
    ]
複製程式碼

說明:fn1函式中this的值,會因不同的執行上下文環境而變化。

以上就是上面程式碼的執行過程,程式碼無任何意義,只為理解執行上下文的作用域鏈。

總結

  • JavaScript中的函式的作用域是在函式定義的時候就已經建立了,屬於靜態作用域,也叫詞法作用域。
  • 同一個作用域下,不同的呼叫會產生不同的執行上下文。
  • 作用域只是一個抽象的概念,具體變數的賦值是在程式碼的執行階段,在執行上下文中。

相關文章