Javascript作用域原理
- 作者: Laruence( )
- 本文地址: http://www.laruence.com/2009/05/28/863.html
- 轉載請註明出處
問題的提出
首先看一個例子:
- var name = 'laruence';
- function echo() {
- alert(name);
- var name = 'eve';
- alert(name);
- alert(age);
- }
- echo();
運行結果是什麼呢?
上面的問題, 我相信會有很多人會認爲是:
- laruence
- eve
- [腳本出錯]
因爲會以爲在echo中, 第一次alert的時候, 會取到全局變量name的值, 而第二次值被局部變量name覆蓋, 所以第二次alert是’eve’. 而age屬性沒有定義, 所以腳本會出錯.
但其實, 運行結果應該是:
- undefined
- eve
- [腳本出錯]
爲什麼呢?
JavaScript的作用域鏈
首先讓讓我們來看看Javasript(簡稱JS, 不完全代表JScript)的作用域的原理: JS權威指南中有一句很精闢的描述: ”JavaScript中的函數運行在它們被定義的作用域裏,而不是它們被執行的作用域裏.”
爲了接下來的知識, 你能順利理解, 我再提醒一下, 在JS中:”一切皆是對象, 函數也是”.
在JS中,作用域的概念和其他語言差不多, 在每次調用一個函數的時候 ,就會進入一個函數內的作用域,當從函數返回以後,就返回調用前的作用域.
JS的語法風格和C/C++類似, 但作用域的實現卻和C/C++不同,並非用“堆棧”方式,而是使用列表,具體過程如下(ECMA262中所述):
任何執行上下文時刻的作用域, 都是由作用域鏈(scope chain, 後面介紹)來實現.
在一個函數被定義的時候, 會將它定義時刻的scope chain鏈接到這個函數對象的[[scope]]屬性.
在一個函數對象被調用的時候,會創建一個活動對象(也就是一個對象), 然後對於每一個函數的形參,都命名爲該活動對象的命名屬性, 然後將這個活動對象做爲此時的作用域鏈(scope chain)最前端, 並將這個函數對象的[[scope]]加入到scope chain中.
看個例子:
- var func = function(lps, rps){
- var name = 'laruence';
- ........
- }
- func();
在執行func的定義語句的時候, 會創建一個這個函數對象的[[scope]]屬性(內部屬性,只有JS引擎可以訪問, 但FireFox的幾個引擎(SpiderMonkey和Rhino)提供了私有屬性__parent__來訪問它), 並將這個[[scope]]屬性, 鏈接到定義它的作用域鏈上(後面會詳細介紹), 此時因爲func定義在全局環境, 所以此時的[[scope]]只是指向全局活動對象window active object.
在調用func的時候, 會創建一個活動對象(假設爲aObj, 由JS引擎預編譯時刻創建, 後面會介紹),並創建arguments屬性, 然後會給這個對象添加倆個命名屬性aObj.lps, aObj.rps; 對於每一個在這個函數中申明的局部變量和函數定義, 都作爲該活動對象的同名命名屬性.
然後將調用參數賦值給形參數,對於缺少的調用參數,賦值爲undefined。
然後將這個活動對象做爲scope chain的最前端, 並將func的[[scope]]屬性所指向的,定義func時候的頂級活動對象, 加入到scope chain.
有了上面的作用域鏈, 在發生標識符解析的時候, 就會逆向查詢當前scope chain列表的每一個活動對象的屬性,如果找到同名的就返回。找不到,那就是這個標識符沒有被定義。
注意到, 因爲函數對象的[[scope]]屬性是在定義一個函數的時候決定的, 而非調用的時候, 所以如下面的例子:
- var name = 'laruence';
- function echo() {
- alert(name);
- }
- function env() {
- var name = 'eve';
- echo();
- }
- env();
運行結果是:
- laruence
結合上面的知識, 我們來看看下面這個例子:
- function factory() {
- var name = 'laruence';
- var intro = function(){
- alert('I am ' + name);
- }
- return intro;
- }
- function app(para){
- var name = para;
- var func = factory();
- func();
- }
- app('eve');
當調用app的時候, scope chain是由: {window活動對象(全局)}->{app的活動對象} 組成.
在剛進入app函數體時, app的活動對象有一個arguments屬性, 倆個值爲undefined的屬性: name和func. 和一個值爲’eve’的屬性para;
此時的scope chain如下:
- [[scope chain]] = [
- {
- para : 'eve',
- name : undefined,
- func : undefined,
- arguments : []
- }, {
- window call object
- }
- ]
當調用進入factory的函數體的時候, 此時的factory的scope chain爲:
- [[scope chain]] = [
- {
- name : undefined,
- intor : undefined
- }, {
- window call object
- }
- ]
注意到, 此時的作用域鏈中, 並不包含app的活動對象.
在定義intro函數的時候, intro函數的[[scope]]爲:
- [[scope chain]] = [
- {
- name : 'laruence',
- intor : undefined
- }, {
- window call object
- }
- ]
從factory函數返回以後,在app體內調用intor的時候, 發生了標識符解析, 而此時的sope chain是:
- [[scope chain]] = [
- {
- intro call object
- }, {
- name : 'laruence',
- intor : undefined
- }, {
- window call object
- }
- ]
因爲scope chain中,並不包含factory活動對象. 所以, name標識符解析的結果應該是factory活動對象中的name屬性, 也就是’laruence’.
所以運行結果是:
- I am laruence
現在, 大家對”JavaScript中的函數運行在它們被定義的作用域裏,而不是它們被執行的作用域裏.”這句話, 應該有了個全面的認識了吧?
Javascript的預編譯
我們都知道,JS是一種腳本語言, JS的執行過程, 是一種翻譯執行的過程.
那麼JS的執行中, 有沒有類似編譯的過程呢?
首先, 我們來看一個例子:
- <script>
- alert(typeof eve); //function
- function eve() {
- alert('I am Laruence');
- };
- </script>
誒? 在alert的時候, eve不是應該還是未定義的麼? 怎麼eve的類型還是function呢?
恩, 對, 在JS中, 是有預編譯的過程的, JS在執行每一段JS代碼之前, 都會首先處理var關鍵字和function定義式(函數定義式和函數表達式).
如上文所說, 在調用函數執行之前, 會首先創建一個活動對象, 然後搜尋這個函數中的局部變量定義,和函數定義, 將變量名和函數名都做爲這個活動對象的同名屬性, 對於局部變量定義,變量的值會在真正執行的時候才計算, 此時只是簡單的賦爲undefined.
而對於函數的定義,是一個要注意的地方:
- <script>
- alert(typeof eve); //結果:function
- alert(typeof walle); //結果:undefined
- function eve() { //函數定義式
- alert('I am Laruence');
- };
- var walle = function() { //函數表達式
- }
- alert(typeof walle); //結果:function
- </script>
這就是函數定義式和函數表達式的不同, 對於函數定義式, 會將函數定義提前. 而函數表達式, 會在執行過程中才計算.
說到這裏, 順便說一個問題 :
- var name = 'laruence';
- age = 26;
我們都知道不使用var關鍵字定義的變量, 相當於是全局變量, 聯繫到我們剛纔的知識:
在對age做標識符解析的時候, 因爲是寫操作, 所以當找到到全局的window活動對象的時候都沒有找到這個標識符的時候, 會在window活動對象的基礎上, 返回一個值爲undefined的age屬性.
也就是說, age會被定義在頂級作用域中.
現在, 也許你注意到了我剛纔說的: JS在執行每一段JS代碼..
對, 讓我們看看下面的例子:
- <script>
- alert(typeof eve); //結果:undefined
- </script>
- <script>
- function eve() {
- alert('I am Laruence');
- }
- </script>
明白了麼? 也就是JS的預編譯是以段爲處理單元的…
揭開謎底
現在讓我們回到我們的第一個問題:
當echo函數被調用的時候, echo的活動對象已經被預編譯過程創建, 此時echo的活動對象爲:
- [callObj] = {
- name : undefined
- }
當第一次alert的時候, 發生了標識符解析, 在echo的活動對象中找到了name屬性, 所以這個name屬性, 完全的遮擋了全局活動對象中的name屬性.