1. 程式人生 > >js基礎--閉包

js基礎--閉包

          從技術的角度講,所有的JavaScript函式都是閉包:它們都是物件,它們都關聯到作用域鏈。定義大多數函式時的作用域鏈在呼叫函式時依然有效,但這並不影響閉包。當呼叫函式時閉包所指向的作用域鏈和定義函式時的作用域鏈不是同一個作用域鏈時,事情就變得非常微妙。當一個函式嵌套了另外一個函式,外部函式將巢狀的函式物件作為返回值返回的時候往往會發生這種事情。有很多強大的程式設計技術都利用到了這類巢狀的函式閉包,以至於這種程式設計模式在JavaScript中非常常見。當你第一次碰到閉包時可能會覺得非常讓人費解,一旦你理解掌握了閉包之後,就能非常自如地使用它了,瞭解這一點至關重要。

 

巢狀函式的作用域規則

理解閉包首先要了解巢狀函式的詞法作用域規則。看一下這段程式碼:


var scope="global scope";//全域性變數
function checkscope(){
    var scope="local scope";//區域性變數
    function f(){return scope;}//在作用域中返回這個值
    return f();
}
checkscope()//=>"local scope"

checkscope()函式聲明瞭一個區域性變數,並定義了一個函式f(),函式f()返回了這個變數的值,最後將函式f()的執行結果返回。你應當非常清楚為什麼呼叫checkscope()會返回"local scope"。現在我們對這段程式碼做一點改動。你知道這段程式碼返回什麼嗎?

var scope="global scope";//全域性變數
function checkscope(){
    var scope="local scope";//區域性變數
    function f(){return scope;}//在作用域中返回這個值
    return f;
}
checkscope()()//返回值是什麼?

 

         在這段程式碼中,我們將函式內的一對圓括號移動到了checkscope()之後。checkscope()現在僅僅返回函式內巢狀的一個函式物件,而不是直接返回結果。在定義函式的作用域外面,呼叫這個巢狀的函式(包含最後一行程式碼的最後一對圓括號)會發生什麼事情呢?

        回想一下詞法作用域的基本規則:JavaScript函式的執行用到了作用域鏈,這個作用域鏈是函式定義的時候建立的。巢狀的函式f()定義在這個作用域鏈裡,其中的變數scope一定是區域性變數,不管在何時何地執行函式f(),這種繫結在執行f()時依然有效。因此最後一行程式碼返回"local scope",而不是"global scope"。簡言之,閉包的這個特性強大到讓人吃驚:它們可以捕捉到區域性變數(和引數),並一直儲存下來,看起來像這些變數繫結到了在其中定義它們的外部函式。

實現閉包

      我們將作用域鏈描述為一個物件列表,不是繫結的棧。每次呼叫JavaScript函式的時候,都會為之建立一個新的物件用來儲存區域性變數,把這個物件新增至作用域鏈中。當函式返回的時候,就從作用域鏈中將這個繫結變數的物件刪除。如果不存在巢狀的函式,也沒有其他引用指向這個繫結物件,它就會被當做垃圾回收掉。如果定義了巢狀的函式,每個巢狀的函式都各自對應一個作用域鏈,並且這個作用域鏈指向一個變數繫結物件。但如果這些巢狀的函式物件在外部函式中儲存下來,那麼它們也會和所指向的變數繫結物件一樣當做垃圾回收。但是如果這個函式定義了巢狀的函式,並將它作為返回值返回或者儲存在某處的屬性裡,這時就會有一個外部引用指向這個巢狀的函式。它就不會被當做垃圾回收,並且它所指向的變數繫結物件也不會被當做垃圾回收.

       uniqueInteger()函式,這個函式使用自身的一個屬性來儲存每次返回的值,以便每次呼叫都能跟蹤上次的返回值。但這種做法有一個問題,就是惡意程式碼可能將計數器重置或者把一個非整數賦值給它,導致uniquenterger()函式不一定能產生“唯一”的“整數”。而閉包可以捕捉到單個函式呼叫的區域性變數,並將這些區域性變數用做私有狀態。我們可以利用閉包這樣來重寫uniqueInteger()函式:

var uniqueInteger=(function(){//定義函式並立即呼叫
    var counter=0;//函式的私有狀態
    return function(){return counter++;};
}());


       你需要仔細閱讀這段程式碼才能理解其含義。粗略來看,第一行程式碼看起來像將函式賦值給一個變數uniqueInteger,實際上,這段程式碼定義了一個立即呼叫的函式(函式的開始帶有左圓括號),因此是這個函式的返回值賦值給變數uniqueInteger。現在,我們來看函式體,這個函式返回另外一個函式,這是一個巢狀的函式,我們將它賦值給變數uniqueInteger,巢狀的函式是可以訪問作用域內的變數的,而且可以訪問外部函式中定義的counter變數。當外部函式返回之後,其他任何程式碼都無法訪問counter變數,只有內部的函式才能訪問到它。

        像counter一樣的私有變數不是隻能用在一個單獨的閉包內,在同一個外部函式內定義的多個巢狀函式也可以訪問它,這多個巢狀函式都共享一個作用域鏈,看一下這段程式碼:

function counter(){
    var n=0;
    return{
        count:function(){return n++;},
        reset:function(){n=0;}
    };
}
var c=counter(),d=counter();//建立兩個計數器
c.count()//=>0
d.count()//=>0:它們互不干擾
c.reset()//reset()和count()方法共享狀態
c.count()//=>0:因為我們重置了c
d.count()//=>1:而沒有重置d


        counter()函式返回了一個“計數器”物件,這個物件包含兩個方法:count()返回下一個整數,reset()將計數器重置為內部狀態。首先要理解,這兩個方法都可以訪問私有變數n。再者,每次呼叫counter()都會建立一個新的作用域鏈和一個新的私有變數。因此,如果呼叫counter()兩次,則會得到兩個計數器物件,而且彼此包含不同的私有變數,呼叫其中一個計數器物件的count()或reset()不會影響到另外一個物件。

         從技術角度看,其實可以將這個閉包合併為屬性存取器方法getter和setter。下面這段程式碼所示的counter()函式的版本是6.6節中程式碼的變種,所不同的是,這裡私有狀態的實現是利用了閉包,而不是利用普通的物件屬性來實現:

function counter(n){//函式引數n是一個私有變數
    return{//屬性getter方法返回並給私有計數器var遞增1
        get count(){return n++;},//屬性setter不允許n遞減
        set count(m){
            if(m>=n)n=m;
        else throw Error("count can only be set to a larger value");
        }
    };
}
var c=counter(1000);
c.count//=>1000
c.count//=>1001
c.count=2000
c.count//=>2000
c.count=2000//=>Error!


需要注意的是,這個版本的counter()函式並未宣告區域性變數,而只是使用引數n來儲存私有狀態,屬性存取器方法可以訪問n。這樣的話,呼叫counter()的函式就可以指定私有變數的初始值了。

      使用閉包技術來共享的私有狀態的通用做法。這個例子定義了addPrivateProperty()函式,這個函式定義了一個私有變數,以及兩個巢狀的函式用來獲取和設定這個私有變數的值。它將這些巢狀函式新增為所指定物件的方法:

利用閉包實現的私有屬性存取器方法

//這個函式給物件o增加了屬性存取器方法
//方法名稱為get<name>和set<name>。如果提供了一個判定函式
//setter方法就會用它來檢測引數的合法性,然後在儲存它
//如果判定函式返回false,setter方法丟擲一個異常
//
//這個函式有一個非同尋常之處,就是getter和setter函式
//所操作的屬性值並沒有儲存在物件o中
//相反,這個值僅僅是儲存在函式中的區域性變數中
//getter和setter方法同樣是區域性函式,因此可以訪問這個區域性變數
//也就是說,對於兩個存取器方法來說這個變數是私有的
//沒有辦法繞過存取器方法來設定或修改這個值

function addPrivateProperty(o,name,predicate){
    var value;//這是一個屬性值
//getter方法簡單地將其返回
    o["get"+name]=function(){return value;};//setter方法首先檢查值是否合法,若不合法就丟擲異常
//否則就將其儲存起來
    o["set"+name]=function(v){
        if(predicate&&!predicate(v))
        throw Error("set"+name+":invalid value"+v);
    else
        value=v;
    };
}

//下面的程式碼展示了addPrivateProperty()方法
var o={};//設定一個空物件
//增加屬性存取器方法getName()和setName()
//確保只允許字串值
addPrivateProperty(o,"Name",function(x){return typeof x=="string";});
o.setName("Frank");//設定屬性值
console.log(o.getName());//得到屬性值
o.setName(0);//試圖設定一個錯誤型別的值


我們已經給出了很多例子,在同一個作用域鏈中定義兩個閉包,這兩個閉包共享同樣的私有變數或變數。這是一種非常重要的技術,但還是要特別小心那些不希望共享的變數往往不經意間共享給了其他的閉包,瞭解這一點也很重要。看一下下面這段程式碼:

//這個函式返回一個總是返回v的函式
function constfunc(v){return function(){return v;};}//建立一個數組用來儲存常數函式
var funcs=[];
for(var i=0;i<10;i++)funcs[i]=constfunc(i);//在第5個位置的元素所表示的函式返回值為5
funcs[5]()//=>5


這段程式碼利用迴圈建立了很多個閉包,當寫類似這種程式碼的時候往往會犯一個錯誤:那就是試圖將迴圈程式碼移入定義這個閉包的函式之內,看一下這段程式碼:

//返回一個函式組成的陣列,它們的返回值是0~9
function constfuncs(){
    var funcs=[];
    for(var i=0;i<10;i++)
    funcs[i]=function(){return i;};
    return funcs;
}
var funcs=constfuncs();
funcs[5]()//返回值是什麼?


       上面這段程式碼建立了10個閉包,並將它們儲存到一個數組中。這些閉包都是在同一個函式呼叫中定義的,因此它們可以共享變數i。當constfuncs()返回時,變數i的值是10,所有的閉包都共享這一個值,因此,陣列中的函式的返回值都是同一個值,這不是我們想要的結果。關聯到閉包的作用域鏈都是“活動的”,記住這一點非常重要。巢狀的函式不會將作用域內的私有成員複製一份,也不會對所繫結的變數生成靜態快照(static snapshot)。

注意

      書寫閉包的時候還需注意一件事情,this是JavaScript的關鍵字,而不是變數。正如之前討論的,每個函式呼叫都包含一個this值,如果閉包在外部函式裡是無法訪問this的,除非外部函式將this轉存為一個變數:

var self=this;//將this儲存至一個變數中,以便巢狀的函式能夠訪問它
繫結arguments的問題與之類似。arguments並不是一個關鍵字,但在呼叫每個函式時都會自動宣告它,由於閉包具有自己所繫結的arguments,因此閉包內無法直接訪問外部函式的引數陣列,除非外部函式將引數陣列儲存到另外一個變數中:

var outerArguments=arguments;//儲存起來以便巢狀的函式能使用它
利用了這種程式設計技巧來定義閉包,以便在閉包中可以訪問外部函式的this和arguments值。