認識javascript範圍

來源:互聯網
上載者:User

標籤:

JavaScript的範圍鏈

這是一個非常重要的知識點了,瞭解了JavaScript的範圍鏈的話,能協助我們理解很多‘異常’問題。

下面我們來看一個小例子,前面我說過的聲明提前的例子。

    var name = ‘樂正闖皇‘;    function echo() {        alert(name);        var name = ‘mm‘;        alert(name);        alert(age);    }    echo();

對於這個例子,沒有接觸過這方面的時候,第一反應是會糾結下,這第一個的name,到底調用全域變數的name,還是函數內部的name呢,如果調用全域的,可是函數內部也用定義和賦值啊, 如果調用函數內部的局部變數的話,那麼他的值是mm嗎?還是引用全域的‘樂正闖皇‘呢?

於是這個小例子就會有這樣的錯誤答案:

    樂正闖皇    mm    [指令碼出錯]

其實不然,知道函數內的提前說明,就知道這是不正確的。

    undefined    mm    [指令碼出錯]

應該是這樣的,那到底為什麼是這個答案呢,提前聲明這又是什麼呢?一切的一切,涉及到JavaScript的範圍鏈。

原理

首先來說說,JavaScript的範圍的原理:

在JavaScript權威指南中有一句很精闢的描述: JavaScript中的函數運行在它們被定義的範圍裡,而不是它們被啟動並執行範圍裡。

另外在JavaScript中有個很重要的概念,那就是: 在JavaScript中,一切皆對象,函數也是。

在JS中,範圍的概念和其他語言差不多, 在每次調用一個函數的時候 ,就會進入一個函數內的範圍,當從函數返回以後,就返回調用前的範圍

JS的文法風格和C/C++類似, 但範圍的實現卻和C/C++不同,並非用“堆棧”方式,而是使用列表,具體過程如下(ECMA262中所述):

  • 任何執行內容時刻的範圍, 都是由範圍鏈(scope chain, 後面介紹)來實現
  • 在一個函數被定義的時候, 會將它定義時刻的scope chain連結到這個函數對象的[[scope]]屬性
  • 在一個函數對象被調用的時候,會建立一個使用中的物件(也就是一個對象), 然後對於每一個函數的形參,都命名為該使用中的物件的命名屬性, 然後將這個使用中的物件做為此時的範圍鏈(scope chain)最前端, 並將這個函數對象的[[scope]]加入到scope chain中.

看個例子吧:

    var func = function(lps, rps){        var name = ‘樂正闖皇‘;        ........    }    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 = ‘樂正闖皇‘;    function echo() {        alert(name);    }    function env() {        var name = ‘mm‘;        echo();    }    env();

他的運行結果是:樂正闖皇

結合上面的知識, 我們來看看下面這個例子,還記得那句JavaScript權威指南中的經典,JavaScript中的函數運行在它們被定義的範圍裡,而不是它們被啟動並執行範圍裡。:

    function factory() {        var name = ‘樂正闖皇‘;        var intro = function(){            alert(‘I am ‘ + name);        }        return intro;    }    function app(para){        var name = para;        var func = factory();        func();    }    app(‘mm‘);

當調用app的時候, scope chain是由: {window使用中的物件(全域)}->{app的使用中的物件} 組成.

在剛進入app函數體時, app的使用中的物件有一個arguments屬性, 其他倆個值為undefined的屬性: name和func. 和一個值為’mm’的屬性para;

此時的scope chain如下:

    [[scope chain]] = [        {            para : ‘mm‘,            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 : ‘樂正闖皇‘,            intor : undefined        }, {            window call object        }    ]

從factory函數返回以後,在app體內調用intor的時候, 發生了標識符解析, 而此時的sope chain是:

    [[scope chain]] = [        {            intro call object        }, {            name : ‘樂正闖皇‘,            intor : undefined        }, {            window call object        }    ]

因為scope chain中,並不包含factory使用中的物件. 所以, name標識符解析的結果應該是factory使用中的物件中的name屬性, 也就是’樂正闖皇’.

所以運行結果是: I am 樂正闖皇

至此,完整的一個運行流程,很清晰的能讀懂“JavaScript中的函數運行在它們被定義的範圍裡,而不是它們被啟動並執行範圍裡。”這句話講的是什麼了。

為瞭解釋上面的一些問題,還得說說JavaScript的先行編譯。

JavaScriptの先行編譯

先行編譯,學過C等的我們都知道,可是問題來了,JavaScript是指令碼語言,JavaScript的執行過程是一種翻譯執行的過程,那在JavaScript的執行中,有沒有類似編譯的過程呢?

如果不是很確定,先通過一個例子:

    alert(typeof fun); //function    function fun() {        alert(‘I am 樂正闖皇‘);    };

這時候彈出來的是?-----我去,是“I am 樂正闖皇”然而這時為什麼呢,為啥不是undefined呢。

恩, 對, 在JS中, 是有先行編譯的過程的, JS在執行每一段JS代碼之前, 都會首先處理var關鍵字和function定義式(函數定義式和函數運算式).

如上文所說, 在調用函數執行之前, 會首先建立一個使用中的物件, 然後搜尋這個函數中的局部變數定義,和函數定義, 將變數名和函數名都做為這個使用中的物件的同名屬性, 對於局部變數定義,變數的值會在真正執行的時候才計算, 此時只是簡單的賦為undefined.

而對於函數的定義,是一個要注意的地方:

    alert(typeof fun); //結果:function    alert(typeof fn); //結果:undefined    function fun() { //函數定義式        alert(‘I am 樂正闖皇‘);    };    var fn = function() { //函數運算式    }    alert(typeof fn); //結果:function

這就是函數定義式和函數運算式的不同, 對於函數定義式, 會將函數定義提前. 而函數運算式, 會在執行過程中才計算.

說到這裡, 順便說一個問題 :

    var name = ‘樂正闖皇‘;    age = 25;

我們都知道不使用var關鍵字定義的變數, 相當於是全域變數, 聯絡到我們剛才的知識:

在對age做標識符解析的時候, 因為是寫操作, 所以當找到到全域的window使用中的物件的時候都沒有找到這個標識符的時候, 會在window使用中的物件的基礎上, 返回一個值為undefined的age屬性.

也就是說, age會被定義在頂級範圍中.

現在, 也許你注意到了我剛才說的: JS在執行每一段JS代碼之前, 都會首先處理var關鍵字和function定義式(函數定義式和函數運算式).

對, 讓我們看看下面的例子:

    < script >        alert(typeof mm); //結果:undefined    < /script >    < script >        function mm() {            alert(‘I am 樂正闖皇‘);        }    < /script >

相關文章閱讀:

《JavaScript變數的範圍》

認識javascript範圍

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.