閉包(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