理解JavaScript的作用域鏈

發表於2015-10-31

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

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

作用域

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

例如,當for迴圈這個程式碼塊結束後,依然可以訪問變數”i”。

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

全域性作用域中的物件可以在程式碼的任何地方訪問,一般來說,下面情況的物件會在全域性作用域中:

  • 最外層函式和在最外層函式外面定義的變數
  • 沒有通過關鍵字”var”宣告的變數
  • 瀏覽器中,window物件的屬性

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

作用域鏈

通過前面一篇文章瞭解到,每一個Execution Context中都有一個VO,用來存放變數,函式和引數等資訊。

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

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

看一個例子:

上面程式碼的輸出結果為”60″,函式bar可以直接訪問”z”,然後通過作用域鏈訪問上層的”x”和”y”。

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

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

第一感覺(錯覺)這段程式碼會輸出”0,1,2″。但是根據前面的介紹,變數”i”是存放在”Global VO”中的變數,迴圈結束後”i”的值就被設定為3,所以程式碼最後的三次函式呼叫訪問的是相同的”Global VO”中已經被更新的”i”。

結合作用域鏈看閉包

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

下面我們就通過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)

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

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

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

總結

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

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

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

相關文章