1. 程式人生 > >JS(8)——閉包(closure)

JS(8)——閉包(closure)

1. 閉包的概念

官方的解釋是:閉包是一個擁有許多變數和綁定了這些變數的環境的表示式(通常是一個函式),因而這些變數也是該表示式的一部分。簡單來說,閉包就是能夠讀取其他函式內部變數的函式。由於在Javascript語言中,只有函式內部的子函式才能讀取區域性變數,因此可以把閉包簡單理解成定義在一個函式內部的函式。 所以,在本質上,閉包就是將函式內部和函式外部連線起來的一座橋樑。

要理解閉包,首先必須理解Javascript特殊的變數作用域。變數的作用域兩種:全域性變數和區域性變數。Javascript語言的特殊之處,就在於函式內部可以直接讀取全域性變數,例如:

var n=999;  
function f1(){  
     alert(n);  
} 
f1(); // 999  

另一方面,在函式外部自然無法讀取函式內的區域性變數。例如:

function f1(){  
     var n=999;  
}  
alert(n); // error  

這裡有一個地方需要注意,函式內部宣告變數的時候,一定要使用var命令。如果不用的話,實際上聲明瞭一個全域性變數,即:

function f1(){  
     n=999;  
} 
f1();  
alert(n); // 999  

出於種種原因,我們有時候需要得到函式內的區域性變數。但是,前面已經說過了,正常情況下,這是辦不到的,只有通過變通方法才能實現。那就是在函式的內部,再定義一個函式。例如:

function f1(){  
    n=999;  
  function f2(){  
      alert(n); // 999  
  }
 }  

既然f2可以讀取f1中的區域性變數,那麼只要把f2作為返回值,就可以在f1外部讀取它的內部變量了。例如:

 function f1(){  
      n=999;  
      function f2(){  
          alert(n);  
    }  

    return f2;  
}  

var result=f1();  
result(); // 999 

 上面的f2函式,就是閉包。閉包其實就是定義在一個函式內部的函式(因為是子函式所以能夠讀取所在父函式的內部變數)。

注意:由於閉包會攜帶包含它的函式的作用域,因此會比其他函式佔用更多的記憶體。

2.  閉包的用途

 1

可以讀取函式內部的變數,保護函式內的變數安全。

 2、在記憶體中維持一個變數。

3. 使用閉包的注意點

1、由於閉包會使得函式中的變數都被儲存在記憶體中,記憶體消耗很大,所以不能濫用閉包,否則會造成網頁的效能問題,在IE中可能導致記憶體洩露。解決方法是,在退出函式之前,將不使用的區域性變數全部刪除。

2、閉包會在父函式外部,改變父函式內部變數的值。所以,如果你把父函式當作物件(object)使用,把閉包當作它的公用方法(Public Method),把內部變數當作它的私有屬性(private value),這時一定要小心,不要隨便改變父函式內部變數的值。

注意:閉包只能取得包含函式中任何變數的最後一個值。閉包所儲存的是整個變數物件,而不是某個特殊的變數。下面這個例子可以清晰地說明這個問題。

function createFunctions(){
    var result = new Array();
    for (var i=0; i < 10; i++){
        result[i] = function(){
            return i;
        };
    }

    return result;
}

這個函式會返回一個函式陣列。表面上看,似乎每個函式都應該返自己的索引值,即位置0 的函式返回0,位置1 的函式返回1,以此類推。但實際上,每個函式都返回10。因為每個函式的作用域鏈中都儲存著createFunctions() 函式的活動物件,

所以它們引用的都是同一個變數i 。當createFunctions()函式返回後,變數i 的值是

10,此時每個函式都引用著儲存變數i 的同一個變數物件,所以在每個函式內部i 的值都是10。但是,我們可以通過建立另一個匿名函式強制讓閉包的行為符合預期,如下所示

function createFunctions(){

    var result = new Array();

    for (var i=0; i < 10; i++){
        result[i] = function(num){

            return function(){
                return num;
                };
        }(i);
    }

    return result;
}

在重寫了前面的createFunctions()函式後,每個函式就會返回各自不同的索引值了。在這個版本中,我們沒有直接把閉包賦值給陣列,而是定義了一個匿名函式,並將立即執行該匿名函式的結果賦給陣列。這裡的匿名函式有一個引數num,也就是最終的函式要返回的值。在呼叫每個匿名函式時,我們傳入了變數i。由於函式引數是按值傳遞的,所以就會將變數i 的當前值複製給引數num。而在這個匿名函式內部,又建立並返回了一個訪問num 的閉包。這樣一來,result 陣列中的每個函式都有自己num 變數的一個副本,因此就可以返回各自不同的數值了。每個函式在被呼叫時都會自動取得兩個特殊變數:this arguments。內部函式在搜尋這兩個變數時,只會搜尋到其活動物件為止,因此永遠不可能直接訪問外部函式中的這兩個變數。不過,把外部作用域中的this 物件儲存在一個閉包能夠訪問到的

變數裡,就可以讓閉包訪問該物件了,例如:

var name = "The Window";

var object = {
    name : "My Object",

    getNameFunc : function(){
        var that = this;

        return function(){
            return that.name;
        };
    }
};

alert(object.getNameFunc()()); //"My Object"

程式碼中突出的行展示了這個例子與前一個例子之間的不同之處。在定義匿名函式之前,我們把this物件賦值給了一個名叫that 的變數。而在定義了閉包之後,閉包也可以訪問這個變數,因為它是我們在包含函式中特意聲名的一個變數。即使在函式返回之後,that 也仍然引用著object,所以呼叫object.getNameFunc()()就返回了"My Object"

arguments也存在同樣的問題。如果想訪問作用域中的arguments 物件,必須將對該物件的引用儲存到另一個閉包能夠訪問的變數中。

注意:在幾種特殊情況下,this 的值可能會意外地改變。比如,下面的程式碼是修改前面例子的結果

var name = "The Window";

var object = {
    name : "My Object",
    getName: function(){
        return this.name;
    }
};

這裡的getName()方法只簡單地返回this.name 的值。以下是幾種呼叫object.getName()的方式以及各自的結果。

object.getName(); //"My Object"

(object.getName)(); //"My Object"

(object.getName = object.getName)(); //"The Window",在非嚴格模式下

第一行程式碼跟平常一樣呼叫了object.getName(),返回的是"My Object",因為this.name就是object.name。第二行程式碼在呼叫這個方法前先給它加上了括號。雖然加上括號之後,就好像只是在引用一個函式,但this 的值得到了維持,因為object.getName (object.getName)的定義是相同的。第三行程式碼先執行了一條賦值語句,然後再呼叫賦值後的結果。因為這個賦值表示式的值是函式本身,所以this 的值不能得到維持,結果就返回了"The Window"