鍍金池/ 教程/ HTML/ 閉包
面向?qū)ο蟮?Javascript
客戶端的 JavaScript
概述
核心概念深入
函數(shù)式的 Javascript
對象與 JSON
前端 JavaScript 框架
基本概念
數(shù)組
閉包
正則表達(dá)式
函數(shù)

閉包

閉包向來給包括 JavaScript 程序員在內(nèi)的程序員以神秘,高深的感覺,事實上,閉包的概念在函數(shù)式編程語言中算不上是難以理解的知識。如果對作用域,函數(shù)為獨立的對象這樣的基本概念理解較好的話,理解閉包的概念并在實際的編程實踐中應(yīng)用則頗有水到渠成之感。

在 DOM 的事件處理方面,大多數(shù)程序員甚至自己已經(jīng)在使用閉包了而不自知,在這種情況下,對于瀏覽器中內(nèi)嵌的 JavaScript 引擎的 bug 可能造成內(nèi)存泄漏這一問題姑且不論,就是程序員自己調(diào)試也常常會一頭霧水。

用簡單的語句來描述 JavaScript 中的閉包的概念:由于 JavaScript 中,函數(shù)是對象,對象是屬性的集合,而屬性的值又可以是對象,則在函數(shù)內(nèi)定義函數(shù)成為理所當(dāng)然,如果在函數(shù) func 內(nèi)部聲明函數(shù) inner,然后在函數(shù)外部調(diào)用 inner,這個過程即產(chǎn)生了一個閉包。

閉包的特性

我們先來看一個例子,如果不了解 JavaScript 的特性,很難找到原因:

var outter = [];  
function clouseTest () {  
    var array = ["one", "two", "three", "four"];  
    for(var i = 0; i < array.length;i++){  
       var x = {};  
       x.no = i;  
       x.text = array[i];  
       x.invoke = function(){  
           print(i);  
       }  
       outter.push(x);  
    }  
}  

//調(diào)用這個函數(shù)  
clouseTest();  

print(outter[0].invoke());  
print(outter[1].invoke());  
print(outter[2].invoke());  
print(outter[3].invoke());

運行的結(jié)果如何呢?很多初學(xué)者可能會得出這樣的答案:

0
1
2
3

然而,運行這個程序,得到的結(jié)果為:

4
4
4
4

其實,在每次迭代的時候,這樣的語句 x.invoke = function(){print(i);}并沒有被執(zhí)行,只是構(gòu)建了一個函數(shù)體為”print(i);”的函數(shù)對象,如此而已。而當(dāng) i=4 時,迭代停止,外部函數(shù)返回,當(dāng)再去調(diào)用 outter[0].invoke()時,i的值依舊為4,因此outter數(shù)組中的每一個元素的invoke都返回i的值:4

如何解決這一問題呢?我們可以聲明一個匿名函數(shù),并立即執(zhí)行它:

var outter = [];  

function clouseTest2(){  
    var array = ["one", "two", "three", "four"];  
    for(var i = 0; i < array.length;i++){  
       var x = {};  
       x.no = i;  
       x.text = array[i];  
       x.invoke = function(no){  
           return function(){  
              print(no);  
           }  
       }(i);  
       outter.push(x);  
    }    
}  

clouseTest2();

這個例子中,我們?yōu)?x.invoke 賦值的時候,先運行一個可以返回一個函數(shù)的函數(shù),然后立即執(zhí)行之,這樣,x.invoke 的每一次迭代器時相當(dāng)與執(zhí)行這樣的語句:

//Java代碼  
//x == 0  
x.invoke = function(){print(0);}  
//x == 1  
x.invoke = function(){print(1);}  
//x == 2  
x.invoke = function(){print(2);}  
//x == 3  
x.invoke = function(){print(3);}  

這樣就可以得到正確結(jié)果了。閉包允許你引用存在于外部函數(shù)中的變量。然而,它并不是使用該變量創(chuàng)建時的值,相反,它使用外部函數(shù)中該變量最后的值。

閉包的用途

現(xiàn)在,閉包的概念已經(jīng)清晰了,我們來看看閉包的用途。事實上,通過使用閉包,我們可以做很多事情。比如模擬面向?qū)ο蟮拇a風(fēng)格;更優(yōu)雅,更簡潔的表達(dá)出代碼;在某些方面提升代碼的執(zhí)行效率。

匿名自執(zhí)行函數(shù)

上一節(jié)中的例子,事實上就是閉包的一種用途,根據(jù)前面講到的內(nèi)容可知,所有的變量,如果不加上var關(guān)鍵字,則默認(rèn)的會添加到全局對象的屬性上去,這樣的臨時變量加入全局對象有很多壞處,比如:別的函數(shù)可能誤用這些變量;造成全局對象過于龐大,影響訪問速度(因為變量的取值是需要從原型鏈上遍歷的)。除了每次使用變量都是用var 關(guān)鍵字外,我們在實際情況下經(jīng)常遇到這樣一種情況,即有的函數(shù)只需要執(zhí)行一次,其內(nèi)部變量無需維護(hù),比如 UI 的初始化,那么我們可以使用閉包:

var datamodel = {  
    table : [],  
    tree : {}  
};  

(function(dm){  
    for(var i = 0; i < dm.table.rows; i++){  
       var row = dm.table.rows[i];  
       for(var j = 0; j < row.cells; i++){  
           drawCell(i, j);  
       }  
    }  

    //build dm.tree    
})(datamodel); 

我們創(chuàng)建了一個匿名的函數(shù),并立即執(zhí)行它,由于外部無法引用它內(nèi)部的變量,因此在執(zhí)行完后很快就會被釋放,關(guān)鍵是這種機(jī)制不會污染全局對象。

緩存

再來看一個例子,設(shè)想我們有一個處理過程很耗時的函數(shù)對象,每次調(diào)用都會花費很長時間,那么我們就需要將計算出來的值存儲起來,當(dāng)調(diào)用這個函數(shù)的時候,首先在緩存中查找,如果找不到,則進(jìn)行計算,然后更新緩存并返回值,如果找到了,直接返回查找到的值即可。閉包正是可以做到這一點,因為它不會釋放外部的引用,從而函數(shù)內(nèi)部的值可以得以保留。

var CachedSearchBox = (function(){  
    var cache = {},  
       count = [];  
    return {  
       attachSearchBox : function(dsid){  
           if(dsid in cache){//如果結(jié)果在緩存中  
              return cache[dsid];//直接返回緩存中的對象  
           }  
           var fsb = new uikit.webctrl.SearchBox(dsid);//新建  
           cache[dsid] = fsb;//更新緩存  
           if(count.length > 100){//保正緩存的大小<=100  
              delete cache[count.shift()];  
           }  
           return fsb;        
       },  

       clearSearchBox : function(dsid){  
           if(dsid in cache){  
              cache[dsid].clearSelection();    
           }  
       }  
    };  
})();  

CachedSearchBox.attachSearchBox("input1");  

這樣,當(dāng)我們第二次調(diào)用 CachedSearchBox.attachSerachBox(“input1”)的時候,我們就可以從緩存中取道該對象,而不用再去創(chuàng)建一個新的 searchbox 對象。

實現(xiàn)封裝

可以先來看一個關(guān)于封裝的例子,在 person 之外的地方無法訪問其內(nèi)部的變量,而通過提供閉包的形式來訪問:

var person = function(){  
    //變量作用域為函數(shù)內(nèi)部,外部無法訪問  
    var name = "default";     

    return {  
       getName : function(){  
           return name;  
       },  
       setName : function(newName){  
           name = newName;  
       }  
    }  
}();  

print(person.name);//直接訪問,結(jié)果為undefined  
print(person.getName());  
person.setName("abruzzi");  
print(person.getName());

得到結(jié)果如下:

undefined
default
abruzzi

閉包的另一個重要用途是實現(xiàn)面向?qū)ο笾械膶ο?,傳統(tǒng)的對象語言都提供類的模板機(jī)制,這樣不同的對象(類的實例)擁有獨立的成員及狀態(tài),互不干涉。雖然 JavaScript 中沒有類這樣的機(jī)制,但是通過使用閉包,我們可以模擬出這樣的機(jī)制。還是以上邊的例子來講:

function Person(){  
    var name = "default";     

    return {  
       getName : function(){  
           return name;  
       },  
       setName : function(newName){  
           name = newName;  
       }  
    }  
};  

var john = Person();  
print(john.getName());  
john.setName("john");  
print(john.getName());  

var jack = Person();  
print(jack.getName());  
jack.setName("jack");  
print(jack.getName());

運行結(jié)果如下:

default
john
default
jack

由此代碼可知,john 和 jack 都可以稱為是 Person 這個類的實例,因為這兩個實例對 name 這個成員的訪問是獨立的,互不影響的。

事實上,在函數(shù)式的程序設(shè)計中,會大量的用到閉包,我們將在第八章討論函數(shù)式編程,在那里我們會再次探討閉包的作用。

應(yīng)該注意的問題

內(nèi)存泄漏

在不同的 JavaScript 解釋器實現(xiàn)中,由于解釋器本身的缺陷,使用閉包可能造成內(nèi)存泄漏,內(nèi)存泄漏是比較嚴(yán)重的問題,會嚴(yán)重影響瀏覽器的響應(yīng)速度,降低用戶體驗,甚至?xí)斐蔀g覽器無響應(yīng)等現(xiàn)象。

JavaScript 的解釋器都具備垃圾回收機(jī)制,一般采用的是引用計數(shù)的形式,如果一個對象的引用計數(shù)為零,則垃圾回收機(jī)制會將其回收,這個過程是自動的。但是,有了閉包的概念之后,這個過程就變得復(fù)雜起來了,在閉包中,因為局部的變量可能在將來的某些時刻需要被使用,因此垃圾回收機(jī)制不會處理這些被外部引用到的局部變量,而如果出現(xiàn)循環(huán)引用,即對象A引用B,B引用C,而C又引用到A,這樣的情況使得垃圾回收機(jī)制得出其引用計數(shù)不為零的結(jié)論,從而造成內(nèi)存泄漏。

上下文的引用

關(guān)于 this 我們之前已經(jīng)做過討論,它表示對調(diào)用對象的引用,而在閉包中,最容易出現(xiàn)錯誤的地方是誤用了 this。在前端 JavaScript 開發(fā)中,一個常見的錯誤是錯將 this 類比為其他的外部局部變量:

$(function(){  
    var con = $("div#panel");  
    this.id = "content";  
    con.click(function(){  
       alert(this.id);//panel  
    });  
});  

此處的 alert(this.id)到底引用著什么值呢?很多開發(fā)者可能會根據(jù)閉包的概念,做出錯誤的判斷:

content

理由是,this.id 顯示的被賦值為 content,而在 click 回調(diào)中,形成的閉包會引用到 this.id,因此返回值為 content。然而事實上,這個 alert 會彈出”panel”,究其原因,就是此處的this,雖然閉包可以引用局部變量,但是涉及到 this 的時候,情況就有些微妙了,因為調(diào)用對象的存在,使得當(dāng)閉包被調(diào)用時(當(dāng)這個panel的click事件發(fā)生時),此處的 this 引用的是 con 這個 jQuery 對象。而匿名函數(shù)中的 this.id = “content”是對匿名函數(shù)本身做的操作。兩個 this 引用的并非同一個對象。

如果想要在事件處理函數(shù)中訪問這個值,我們必須做一些改變:

$(function(){  
    var con = $("div#panel");  
    this.id = "content";  
    var self = this;  
    con.click(function(){  
       alert(self.id);//content  
    });  
});  

這樣,我們在事件處理函數(shù)中保存的是外部的一個局部變量 self 的引用,而并非 this。這種技巧在實際應(yīng)用中多有應(yīng)用,我們在后邊的章節(jié)里進(jìn)行詳細(xì)討論。關(guān)于閉包的更多內(nèi)容,我們將在第九章詳細(xì)討論,包括討論其他命令式語言中的“閉包”,閉包在實際項目中的應(yīng)用等等。