閉包兩三事

閉包(closure)是Javascript語言的一個難點,也是它的特色,很多高級應用都要依靠閉包實現。
下面就是我的學習筆記,對於Javascript初學者應該是很有用的。

一、變量的作用域

要理解閉包,首先必須理解Javascript特殊的變量作用域。
變量的作用域無非就是兩種:全局變量和局部變量。

Javascript語言的特殊之處,就在於函數內部可以直接讀取全局變量。

	var n=999;//函數外邊用var聲明的變量爲全局變量
	//函數的聲明
  function f1(){
    alert(n);//顯示全局變量
  }
  f1(); // 函數的調用:結果爲:999

另一方面,在函數外部自然無法讀取函數內的局部變量。

	function f1(){
    var n=999;//函數內部用var聲明的變量爲局部變量
  }
  alert(n); //訪問函數內部的局部變量會報錯:JavaScript error: Uncaught ReferenceError: n is not defined

這裏有一個地方需要注意,函數內部聲明變量的時候,一定要使用var命令。如果不用的話,你實際上聲明瞭一個全局變量!

	function f1(){
    n=999;//聲明一個全局變量並賦值
  }
  f1();//執行函數
  alert(n); //結果爲:999

二、如何從外部讀取局部變量?

出於種種原因,我們有時候需要得到函數內的局部變量。
但是,前面已經說過了,正常情況下,這是辦不到的,只有通過變通方法才能實現。

那就是在函數的內部,再定義一個函數。

	function f1(){
    var n=999;//定義一個局部變量
    //在函數內部定義另一個函數,該函數可以訪問其外部函數中聲明的局部變量
    function f2(){
      alert(n); //結果爲:999
    }
  }

在上面的代碼中,函數f2()就被包括在函數f1()內部,這時f1()內部的所有局部變量,對f2()都是可見的。但是反過來就不行,f2()內部的局部變量,對f1()就是不可見的。這就是Javascript語言特有的"鏈式作用域"結構(chain scope),子對象會一級一級地向上尋找所有父對象的變量。所以,父對象的所有變量,對子對象都是可見的,反之則不成立。

既然f2()可以讀取f1()中的局部變量,那麼只要把f2()作爲返回值,我們不就可以在f1()外部讀取它的內部變量了嗎!

	function f1(){
    var n=999;//f1中定義的局部變量
    function f2(){
      alert(n); //訪問f1中定義的局部變量n
    }
    return f2;//將f2返回
  }
  var result=f1();//f1執行後將f2()返回賦給result,但是此時f2()並未執行
  result(); //結果爲:999

三、閉包的概念

上一節代碼中的f2()函數,就是閉包。

各種專業文獻上的"閉包"(closure)定義非常抽象,很難看懂。

我的理解是,閉包就是能夠讀取其他函數內部變量的函數。

由於在Javascript語言中,只有函數內部的子函數才能讀取局部變量,因此可以把閉包簡單理解成:

"定義在一個函數內部的函數"。所以,在本質上,閉包就是將函數內部和函數外部連接起來的一座橋樑。

四、閉包的用途

閉包可以用在許多地方。它的最大用處有兩個:

一個是前面提到的可以讀取函數內部的變量,另一個就是讓這些變量的值始終保持在內存中。

怎麼來理解這句話呢?請看下面的代碼。

	function f1(){
    var n=999;//f1()中聲明的一個局部變量並賦值
    nAdd=function(){n+=1}//將匿名函數賦值給全局變量nAdd
    function f2(){
      alert(n);//訪問f1()中聲明的局部變量n
    }
    return f2;
  }
  var result=f1();//執行f1之後,返回值爲f2(),因此result實質上是對f2()的一個引用,但是f2()此處還未執行
  result();//執行f2(),結果爲:999
  nAdd();//由於執行f1()的時候,nAdd=function(){n+=1}只類似於一個賦值操作,故並未執行。此處執行結果爲:1000
  result();//再次執行f2(),因爲上一行中nAdd()已經對局部變量n進行加1操作,故結果爲:1000

在這段代碼中,result實際上就是閉包f2()函數。它一共運行了兩次,第一次的值是999,第二次的值是1000。這證明了,函數f1()中的局部變量n一直保存在內存中,並沒有在f1()調用後被自動清除。

爲什麼會這樣呢?原因就在於f1()是f2()的父函數,而f2()被賦給了一個全局變量,這導致f2()始終在內存中,而f2()的存在依賴於f1(),因此f1()也始終在內存中,不會在調用結束後被垃圾回收機制(garbage collection)回收。

這段代碼中另一個值得注意的地方,就是"nAdd=function(){n+=1}"這一行,首先在nAdd前面沒有使用var關鍵字,因此nAdd是一個全局變量,而不是局部變量。其次,nAdd的值是一個匿名函數(anonymous function),而這個匿名函數本身也是一個閉包,所以nAdd相當於是一個setter,可以在函數外部對函數內部的局部變量進行操作。

五、使用閉包的注意點

1)由於閉包會使得函數中的變量都被保存在內存中,內存消耗很大,所以不能濫用閉包,否則會造成網頁的性能問題,在IE中可能導致內存泄露。解決方法是,在退出函數之前,將不使用的局部變量全部刪除。

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

六、思考題

如果你能理解下面幾段代碼的運行結果,應該就算理解閉包的運行機制了。

代碼片段一。

	var name = "The Window";//聲明一個命名爲name的全局變量,並賦值爲The Window
  var object = {
    name : "My Object",//object對象包含一個名爲name的屬性,其值爲:My Object
    getNameFunc : function(){
      return function(){
        return this.name;//this代表的是當前執行代碼的環境對象
      };
    }
  };
  alert(object.getNameFunc()());//結果爲:The Window

解析:

首先object.getNameFunc()函數執行後返回一個匿名函數,此時匿名函數中的this指代的是當前的對象window,因此匿
名函數中this.name爲全局變量name,即結果爲:The Window。

代碼片段二。

	var name = "The Window";//聲明一個命名爲name的全局變量,並賦值爲The Window
  var object = {
    name : "My Object",//object對象包含一個名爲name的屬性,其值爲:My Object
    getNameFunc : function(){
      var that = this;//此處的this指代的是當前對象object,故that也爲object,保存this的值,避免指代轉移
      return function(){
        return that.name;//that代表object
      };
    }
  };
  alert(object.getNameFunc()());//結果爲:My Object

解析:

當執行完object.getNameFunc()函數後,返回一個匿名函數,由於在getNameFunc中將this的值(object)保存於that
中,因此匿名函數中的that.name便爲object.name,故結果爲:My Object。

代碼片段三。

	function fun(n,x){
		document.write("<br>x="+x+"<br>");//在頁面上打印fun()的第二個參數的值,若未傳則爲undefined
		return {
			fun:function(m){
				return fun(m,n);
			}	
		};
	}
	var a = fun(0);//undefined
	a.fun(1);//0
	a.fun(2);//0
	a.fun(3);//0
	
	var b = fun(0).fun(1).fun(2).fun(3);//undefined/0/1/2
	
	var c = fun(0).fun(1);//undefined/0
	c.fun(2);//1
	c.fun(3);//1

解析:

var a = fun(0);----在開始執行fun(0)函數時,實參0傳給第一個參數n,第二個參數x未傳值,故x爲:undefined。
				   當fun()函數執行完成後,將包含fun方法的對象返回給全局變量a,故此處a爲一個對象。【因爲
				   返回對象a的fun(m,n)方法中用到其外層函數fun()中的局部變量n(函數中的參數也是函數中的局
				   部變量),因此在fun()函數調用結束之後局部變量n不會被立即銷燬,故n一直保存於內存中,其
				   值爲在執行fun(0)時傳的參數0。】
a.fun(1)-----------當執行a.fun(1)方法時,實參1傳給m,返回值爲函數fun(m,n),因爲n=0,故執行fun(m,n)即等
				   價於fun(1,0),因此打印結果爲:x=0。【因爲a.fun(1)的返回值爲函數fun(1,0),故當fun(1,0)
				   執行結束之後會返回另一個新的對象,此新對象的參數n的值爲此處傳參1,因此此次執行返回函數
				   fun(1,0)時並不影響a對象中fun()方法中n的值。】
a.fun(2)-----------當執行a.fun(2)方法時,實參2傳給m,返回值爲函數fun(m,n),因爲n=0,故執行fun(m,n)即等
				   價於fun(2,0),因此打印結果爲:x=0。
a.fun(3)-----------執行結果爲:x=0,原因同a.fun(2)。

var b = fun(0).fun(1).fun(2).fun(3);

相當於a.fun(1),返回值爲fun(m,n),即fun(1,0),故結果爲:    相當於c.fun(3),返回值爲fun(m,n),即fun(3,2),
x=0。此次執行函數fun(1,0)後返回值爲一個新的包含fun(m)     故結果爲: x=2。此次執行函數fun(3,2)後返回值
方法的對象,假設爲b,同樣新對象的局部變量n值不會因爲函      爲一個新的包含fun(m)方法的對象,並將該對象最終
數fun(1,0)執行結束而銷燬,此時n=1。                       賦值給全局變量b。
                                              .             .
                                              .             .
                                              .             .
                                     fun(0).fun(1).fun(2).fun(3)
                                       .             .
                                       .             .
                                       .             .
相當於fun(0,undefined),故結果爲:x=undefined    相當於b.fun(2),返回值爲fun(m,n),即fun(2,1),故結果    
返回一個包含fun(m)方法的新對象,假設爲a,由於      爲:x=1。本次執行返回函數fun(2,1)之後返回值爲一個新
a對象的fun(m)方法中訪問了其外層函數中的局部變     的包含fun(m)方法的對象,假設爲c,同樣新對象的n值不會
量n,因此n=0將保存在內存中,不會在函數fun(0)      因爲函數fun(2,1)執行結束而銷燬,此時n=2。
執行結束後銷燬;

var c = fun(0).fun(1);

從全局變量b的結果,可知var c = fun(0).fun(1)的結果分別爲:x=undefined 和 x=1。
從全局變量a的結果,可知c.fun(2);以及c.fun(3);的結果均爲:x=1。

參考文獻

原文鏈接:http://www.ruanyifeng.com/blog/2009/08/learning_javascript_closures.html
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章