上一篇文章中介紹了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屬性。