理解JavaScript的作用域鏈


上一篇文章中介紹了Execution Context中的三個重要部分:VO/AO,scope chain和this,並詳細的介紹了VO/AO在JavaScript代碼執行中的表現。

本文就看看Execution Context中的scope chain。

作用域

開始介紹作用域鏈之前,先看看JavaScript中的作用域(scope)。在很多語言中(C++,C#,Java),作用域都是通過代碼塊(由{}包起來的代碼)來決定的,但是,在JavaScript作用域是跟函數相關的,也可以說成是function-based。

例如,當for循環這個代碼塊結束后,依然可以訪問變量"i"。

for(var i = 0; i < 3; i++){
    console.log(i);
}

console.log(i); //3

對於作用域,又可以分為全局作用域(Global scope)和局部作用域(Local scpoe)。

全局作用域中的對象可以在代碼的任何地方訪問,一般來說,下面情況的對象會在全局作用域中:

  • 最外層函數和在最外層函數外面定義的變量
  • 沒有通過關鍵字"var"聲明的變量
  • 瀏覽器中,window對象的屬性

局部作用域又被稱為函數作用域(Function scope),所有的變量和函數只能在作用域內部使用。

var foo = 1;
window.bar = 2;

function baz(){
    a = 3;
    var b = 4;
}
// Global scope: foo, bar, baz, a 
// Local scope: b

作用域鏈

通過前面一篇文章了解到,每一個Execution Context中都有一個VO,用來存放變量,函數和參數等信息。

在JavaScript代碼運行中,所有用到的變量都需要去當前AO/VO中查找,當找不到的時候,就會繼續查找上層Execution Context中的AO/VO。這樣一級級向上查找的過程,就是所有Execution Context中的AO/VO組成了一個作用域鏈。

所以說,作用域鏈與一個執行上下文相關,是內部上下文所有變量對象(包括父變量對象)的列表,用於變量查詢。

Scope = VO/AO + All Parent VO/AOs

看一個例子:

var x = 10;

function foo() {
    var y = 20;
    
    function bar() {
        var z = 30;
       
        console.log(x + y + z);
    };
    
    bar()
};

foo();

上面代碼的輸出結果為"60",函數bar可以直接訪問"z",然后通過作用域鏈訪問上層的"x"和"y"。

  • 綠色箭頭指向VO/AO
  • 藍色箭頭指向scope chain(VO/AO + All Parent VO/AOs)

再看一個比較典型的例子:

var data = [];
for(var i = 0 ; i < 3; i++){
    data[i]=function() {
        console.log(i);
    }
}

data[0]();// 3
data[1]();// 3
data[2]();// 3

第一感覺(錯覺)這段代碼會輸出"0,1,2"。但是根據前面的介紹,變量"i"是存放在"Global VO"中的變量,循環結束后"i"的值就被設置為3,所以代碼最后的三次函數調用訪問的是相同的"Global VO"中已經被更新的"i"。

結合作用域鏈看閉包

在JavaScript中,閉包跟作用域鏈有緊密的關系。相信大家對下面的閉包例子一定非常熟悉,代碼中通過閉包實現了一個簡單的計數器。

function counter() {
    var x = 0;
    
    return {
        increase: function increase() { return ++x; },
        decrease: function decrease() { return --x; }
    };
}

var ctor = counter();

console.log(ctor.increase());
console.log(ctor.decrease());

下面我們就通過Execution Context和scope chain來看看在上面閉包代碼執行中到底做了哪些事情。

1. 當代碼進入Global Context后,會創建Global VO

  • 綠色箭頭指向VO/AO
  • 藍色箭頭指向scope chain(VO/AO + All Parent VO/AOs)

 

2. 當代碼執行到"var cter = counter();"語句的時候,進入counter Execution Context;根據上一篇文章的介紹,這里會創建counter AO,並設置counter Execution Context的scope chain

 

3. 當counter函數執行的最后,並退出的時候,Global VO中的ctor就會被設置;這里需要注意的是,雖然counter Execution Context退出了執行上下文棧,但是因為ctor中的成員仍然引用counter AO(因為counter AO是increase和decrease函數的parent scope),所以counter AO依然在Scope中。

 

4. 當執行"ctor.increase()"代碼的時候,代碼將進入ctor.increase Execution Context,並為該執行上下文創建VO/AO,scope chain和設置this;這時,ctor.increase AO將指向counter AO。

  • 綠色箭頭指向VO/AO
  • 藍色箭頭指向scope chain(VO/AO + All Parent VO/AOs)
  • 紅色箭頭指向this
  • 黑色箭頭指向parent VO/AO

 

相信看到這些,一定會對JavaScript閉包有了比較清晰的認識,也了解為什么counter Execution Context退出了執行上下文棧,但是counter AO沒有銷毀,可以繼續訪問。

二維作用域鏈查找

通過上面了解到,作用域鏈(scope chain)的主要作用就是用來進行變量查找。但是,在JavaScript中還有原型鏈(prototype chain)的概念。

由於作用域鏈和原型鏈的相互作用,這樣就形成了一個二維的查找。

對於這個二維查找可以總結為:當代碼需要查找一個屬性(property)或者描述符(identifier)的時候,首先會通過作用域鏈(scope chain)來查找相關的對象;一旦對象被找到,就會根據對象的原型鏈(prototype chain)來查找屬性(property)

下面通過一個例子來看看這個二維查找:

var foo = {}

function baz() {

    Object.prototype.a = 'Set foo.a from prototype';

    return function inner() {
        console.log(foo.a);
    }

}

baz()(); 
// Set bar.a from prototype

對於這個例子,可以通過下圖進行解釋,代碼首先通過作用域鏈(scope chain)查找"foo",最終在Global context中找到;然后因為"foo"中沒有找到屬性"a",將繼續沿着原型鏈(prototype chain)查找屬性"a"。

  • 藍色箭頭表示作用域鏈查找
  • 橘色箭頭表示原型鏈查找

總結

本文介紹了JavaScript中的作用域以及作用域鏈,通過作用域鏈分析了閉包的執行過程,進一步認識了JavaScript的閉包。

同時,結合原型鏈,演示了JavaScript中的描述符和屬性的查找。

下一篇我們就看看Execution Context中的this屬性。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM