上一篇文章中介紹了Execution Context中的三個重要部分:VO/AO,scope chain和this,並詳細的介紹了VO/AO在JavaScript程式碼執行中的表現。
本文就看看Execution Context中的scope chain。
作用域
開始介紹作用域鏈之前,先看看JavaScript中的作用域(scope)。在很多語言中(C++,C#,Java),作用域都是通過程式碼塊(由{}包起來的程式碼)來決定的,但是,在JavaScript作用域是跟函式相關的,也可以說成是function-based。
例如,當for迴圈這個程式碼塊結束後,依然可以訪問變數”i”。
1 2 3 4 5 |
for(var i = 0; i < 3; i++){ console.log(i); } console.log(i); //3 |
對於作用域,又可以分為全域性作用域(Global scope)和區域性作用域(Local scpoe)。
全域性作用域中的物件可以在程式碼的任何地方訪問,一般來說,下面情況的物件會在全域性作用域中:
- 最外層函式和在最外層函式外面定義的變數
- 沒有通過關鍵字”var”宣告的變數
- 瀏覽器中,window物件的屬性
區域性作用域又被稱為函式作用域(Function scope),所有的變數和函式只能在作用域內部使用。
1 2 3 4 5 6 7 8 9 |
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組成了一個作用域鏈。
所以說,作用域鏈與一個執行上下文相關,是內部上下文所有變數物件(包括父變數物件)的列表,用於變數查詢。
1 |
Scope = VO/AO + All Parent VO/AOs |
看一個例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
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)
再看一個比較典型的例子:
1 2 3 4 5 6 7 8 9 10 |
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中,閉包跟作用域鏈有緊密的關係。相信大家對下面的閉包例子一定非常熟悉,程式碼中通過閉包實現了一個簡單的計數器。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
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)。
下面通過一個例子來看看這個二維查詢:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
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屬性。