Javascript 的詞法範圍、調用對象和閉包

來源:互聯網
上載者:User

http://blog.belltoy.net/javascript_scope_call_object_closure.html

昨天又有人問我 Javascript 閉包到底是什麼,我發現三言兩語無法解釋清楚,乾脆就寫出來吧。其實關於 Javascript 的函數範圍、調用對象和閉包之間的關係很微妙,關於它們的文章已經有很多,但不知道為什麼很多新手都難以理解。我就嘗試用比較通俗的語言來表達我自己的理解吧。

範圍 Scope

Javascript 中的函數屬於詞法範圍,也就是說函數在它被定義時的範圍中運行而不是在被執行時的範圍內運行。這是犀牛書上的說法。但“定義時”和“執行(被調用)時”這兩個東西有些人搞不清楚。簡單來說,一個函數A在“定義時”就是 function A(){} 這個語句執行的時候就是定義這個函數的時候,而A被調用的時候是 A() 這個語句執行的時候。這兩個概念一定要分清楚。

那詞法範圍(以下稱之為“範圍”,除非特別指明)到底是什麼呢?它是個抽象的概念,說白了它就是一個“範圍”,scope 在英文裡就是範圍的意思。一個函數的範圍是它被定義時它所處的“範圍”,也就是它外層的“範圍”,這個“範圍”包含了外層的變數屬性,這個“範圍”被設定成這個函數的一個內部狀態。一個全域函數被定義的時候,全域(這個函數的外層)的“範圍”就被設定成這個全域函數的一個內部狀態。一個嵌套函數被定義的時候,被嵌套函數(外層函數)的“範圍”就被設定成這個嵌套函數的一個內部狀態。這個“內部狀態”實際上可以理解成範圍鏈,見下文。

當一個函數被調用時,在這個函數裡,它能訪問到它的這個內部狀態,也就可以訪問整個範圍鏈上的所有變數,當然也就包括了外部變數。(實際上是從“調用對象鏈”裡訪問到的。好像有問題吧,請接著閱讀。)

照以上說法,一個函數的範圍是它被定義的時候所處的“範圍”,那麼 Javascript 裡的函數範圍是在函數被定義的時候就確定了,所以它是靜態範圍,詞法範圍又稱為靜態範圍。

調用對象 Call Object

一個函數的調用對象是動態,它是在這個函數被調用時才被執行個體化的。我們已經知道,當一個函數被定義的時候,已經確定了它的範圍鏈。當 Javascript 解譯器調用一個函數的時候,它會添加一個新的對象(調用對象)到這個範圍鏈的前面。這個調用對象的一個屬性被初始化成一個名叫 arguments 的屬性,它引用了這個函數的 Arguments 對象,Arguments 對象是函數的實際參數。所有用 var 語句聲明的本地變數也被定義在這個調用對象裡。這個時候,調用對象處在範圍鏈的頭部,本地變數、函數形式參數和 Arguments 對象全部都在這個函數的範圍裡了。當然,這個時候本地變數、函數形式參數和 Arguments 對象就覆蓋了範圍鏈裡同名的屬性。

範圍、範圍鏈和調用對象之間的關係

我的理解是,範圍是是抽象的,而調用對象是執行個體化的。

在函數被定義的時候,實際上也是它外層函數執行的時候,它確定的範圍鏈實際上是它外層函數的調用對象鏈;當函數被調用時,它的範圍鏈是根據定義的時候確定的範圍鏈(它外層函數的調用對象鏈)加上一個執行個體化的調用對象。所以函數的範圍鏈實際上是調用對象鏈。在一個函數被調用的時候,它的範圍鏈(或者稱調用對象鏈)實際上是它在被定義的時候確定的範圍鏈的一個超集。

它們之間的關係可以表示成:範圍⊃範圍鏈⊇調用對象

太繞口了,舉例說明吧:

123456
function f(x) {    var g = function () { return x; }    return g;}var g1 = f(1);alert(g1());  //輸出 1

假設我們把全域看成類似以下這樣的一個大匿名函數:

123
(function() {    //這裡是全域範圍})();

那麼例子就可以看成是:

12345678
(function() {    function f(x) {        var g = function () { return x; }        return g;    }    var g1 = f(1);    alert(g1());  //輸出 1})();
  1. 全域的大匿名函數被定義的時候,它沒有外層,所以它的範圍鏈是空的。
  2. 全域的大匿名函數直接被執行,全域的範圍鏈裡只有一個 '全域調用對象'。
  3. 函數 f 被定義,此時函數 f 的範圍鏈是它外層的範圍鏈,即 '全域調用對象'。
  4. 函數 f(1) 被執行,它的範圍鏈是新的 f(1) 調用對象加上函數 f 被定義的時候的範圍鏈,即 'f(1) 調用對象->全域調用對象'。
  5. 函數 g (它要被返回給 g1,就命名為 g1吧)在 f(1) 中被定義,它的範圍鏈是它外層的函數 f(1) 的範圍鏈,即 'f(1) 調用對象->全域調用對象'。
  6. 函數 f(1) 返回函數 g 的定義給 g1。
  7. 函數 g1 被執行,它的範圍鏈是新的 g(1) 調用對象加上外層 f(1) 的範圍鏈,即 'g1 調用對象->f(1)調用對象->全域調用對象'。

這樣看就很清楚了吧。

閉包 Closuer

閉包的一個簡單的說法是,當嵌套函數在被嵌套函數之外調用的時候,就形成了閉包。

之前的那個例子其實就是一個閉包。g1 是在 f(1) 內部定義的,卻在 f(1) 返回後才被執行。可以看出,閉包的一個效果就是被嵌套函數 f 返回後,它內部的資源不會被釋放。在外部調用 g 函數時,g 可以訪問 f 的內部變數。根據這個特性,可以寫出很多優雅的代碼。

例如要在一個頁面上作一個統一的計數器,如果用閉包的寫法,可以這麼寫:

1234567891011
var counter  = (function() {    var i = 0;    var fns = {"get": function() {return i;},               "inc": function() {return ++i;}};    return fns;})();//do somethingcounter.inc();//do something elsecounter.inc();var c_value = counter.get();  //now c_value is 2

這樣,在記憶體中就維持了一個變數 i,整個程式中的其它地方都無法直接操作 i 的值,只能通過 counter 的兩個操作。

在 setTimeout(fn, delay) 的時候,我們不能給 fn 這個函數控制代碼傳參數,但可以通過閉包的方法把需要的參數綁定到 fn 內部。

12345
for(var i=0,delay=1000; i< 5; i++, delay +=1000) {    setTimeout(function() {        console.log('i:' + i + " delay:" + delay);    }, delay);}

這樣,列印出來的值都是

i:5 delay:6000i:5 delay:6000i:5 delay:6000i:5 delay:6000i:5 delay:6000

改用閉包的方式可以很容易綁定要傳進去的參數:

1234567
for(var i=0, delay=1000; i < 5; i++, delay += 1000) {    (function(a, _delay) {         setTimeout(function() {             console.log('i:'+a+" delay:"+_delay);        }, _delay);    })(i, delay);}

輸出:

i:0 delay:1000i:1 delay:2000i:2 delay:3000i:3 delay:4000i:4 delay:5000

閉包還有一個很常用的地方,就是在綁定事件的回呼函數的時候。也是同樣的道理,綁定的函數控制代碼不能做參數,但可以通過閉包的形式把參數綁定進去。

總結
  1. 函數的詞法範圍和範圍鏈是不同的東西,詞法範圍是抽象概念,範圍鏈是執行個體化的調用對象鏈。
  2. 函數在被定義的時候,同時也是它外層的函數在被執行的時候。
  3. 函數在被定義的時候它的詞法範圍就已經確定了,但它仍然是抽象的概念,沒有也不能被執行個體化。
  4. 函數在被定義的時候還確定了一個東西,就是它外層函數的範圍鏈,這個是執行個體化的東西。
  5. 函數在被多次調用的時候,它的範圍鏈都是不同的。
  6. 閉包很強大。犀牛書說得對,理解了這些東西,你就可以自稱是進階 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.