內(nèi)存泄漏
由于IE9之前的版本對(duì)JScript對(duì)象和COM對(duì)象使用不同的垃圾收集例程,因此閉包在IE的這些版本中會(huì)導(dǎo)致一些特殊的問(wèn)題。具體來(lái)說(shuō),如果閉包的作用域鏈中保存著一個(gè)HTML元素,那么就意味著該元素將無(wú)法被銷(xiāo)毀:
function assignHandler() {
var element = document.getElementById("someElement");
element.onclick = function () {
alert(element.id);
};
}
以上代碼創(chuàng)建了一個(gè)作為element元素事件處理程序的閉包,而這個(gè)閉包則又創(chuàng)建了一個(gè)循環(huán)引用。由于匿名函數(shù)保存了一個(gè)對(duì)assignHandler()的活動(dòng)對(duì)象的引用,因此就會(huì)導(dǎo)致無(wú)法減少element的引用數(shù)。只要匿名函數(shù)存在,element的引用數(shù)至少也是1,因此它所占用的內(nèi)存就永遠(yuǎn)不會(huì)被回收。不過(guò),這個(gè)問(wèn)題可以通過(guò)稍微改寫(xiě)一下代碼來(lái)解決:
function assignHandler() {
var element = document.getElementById("someElement");
var id = element.id;
element.onclick = function () {
alert(id);
};
element = null;
}
在上面的代碼中,通過(guò)把element.id的一個(gè)副本保存在一個(gè)變量中,并且在閉包中引用該變量消除了循環(huán)引用。但僅僅做到這一步,還是不能解決內(nèi)存泄漏的問(wèn)題。必須要記?。洪]包會(huì)引用包含函數(shù)的整個(gè)活動(dòng)對(duì)象,而其中包含著element。即使閉包不直接引用element,包含函數(shù)的活動(dòng)對(duì)象中也仍然會(huì)保存一個(gè)引用。因此,有必要把element變量設(shè)置為null。這樣就能夠接觸對(duì)DOM對(duì)象的引用,順利地減少其引用數(shù),確保正?;厥掌湔加玫膬?nèi)存。
模仿塊級(jí)作用域
JavaScript沒(méi)有塊級(jí)作用域的概念,這意味著在塊語(yǔ)句中定義的變量,實(shí)際上是在包含函數(shù)中而非語(yǔ)句中創(chuàng)建的:
function outputNumbers(count) {
for (var i = 0; i < count; i++) {
alert(i);
}
alert(i); //計(jì)數(shù)
}
在JavaScript中,變量i是定義在ouputNumbers()的活動(dòng)對(duì)象中的,因此從它有定義開(kāi)始,就可以在函數(shù)內(nèi)部隨處訪(fǎng)問(wèn)它。即使像下面這樣錯(cuò)誤地重新聲明同一個(gè)變量,也不會(huì)改變它的值:
function outputNumbers(count) {
for (var i = 0; i < count; i++) {
alert(i);
}
var i; //重新聲明變量
alert(i); //計(jì)數(shù)
}
JavaScript從來(lái)不會(huì)告訴你是否多次聲明了同一個(gè)變量;遇到這種情況,它只會(huì)對(duì)后續(xù)的聲明視而不見(jiàn)(不過(guò),它會(huì)執(zhí)行后續(xù)聲明中的變量初始化)。匿名函數(shù)可以用來(lái)模仿塊作用域并避免這個(gè)問(wèn)題。
用作塊級(jí)作用域(通常稱(chēng)為私有作用域)的匿名函數(shù)的語(yǔ)法如下:
(function(){
//這里是塊級(jí)作用域
})();
function(){
//從這里是塊級(jí)作用域
}(); //出錯(cuò)!
上一段代碼會(huì)導(dǎo)致語(yǔ)法錯(cuò)誤,因?yàn)镴avaScript將function關(guān)鍵字當(dāng)做一個(gè)函數(shù)聲明的開(kāi)始,而函數(shù)聲明后面不能跟圓括號(hào)。然而,函數(shù)表達(dá)式的后面可以跟圓括號(hào)。
無(wú)論在什么地方,只要臨時(shí)需要一些變量,就可以使用私有作用域:
function outputNumbers(count) {
(function () {
for (var i = 0; i < count; i++) {
alert(i);
}
})();
alert(i); //導(dǎo)致一個(gè)錯(cuò)誤!
}
這種技術(shù)經(jīng)常在全局作用域中被用在函數(shù)外部,從而限制向全局作用域中添加過(guò)多的變量和函數(shù)。一般來(lái)說(shuō),我們都應(yīng)該盡量少向全局作用域中添加變量和函數(shù)。在一個(gè)由很多開(kāi)發(fā)人員共同參與的大型應(yīng)用程序中,過(guò)多的全局變量和函數(shù)很容易導(dǎo)致命名沖突。而通過(guò)創(chuàng)建私有作用域,每個(gè)開(kāi)發(fā)人員既可以使用自己的變量,又不必?fù)?dān)心搞亂全局作用域。
這種做法可以減少閉包占用的內(nèi)存問(wèn)題,因?yàn)闆](méi)有指向匿名函數(shù)的引用。只要函數(shù)執(zhí)行完畢,就可以立即銷(xiāo)毀其作用域鏈了。
私有變量
嚴(yán)格來(lái)講,JavaScript中沒(méi)有私有成員的概念;所有對(duì)象屬性都是公有的。不過(guò),倒是有一個(gè)私有變量的概念。任何在函數(shù)中定義的變量,都可以認(rèn)為是私有變量,因?yàn)椴荒茉诤瘮?shù)的外部訪(fǎng)問(wèn)這些變量。私有變量包括函數(shù)的參數(shù)、局部變量和在函數(shù)內(nèi)部定義的其他函數(shù)。
私有變量在函數(shù)外部不能訪(fǎng)問(wèn)它們。如果在這個(gè)函數(shù)內(nèi)部創(chuàng)建一個(gè)閉包,那么閉包通過(guò)自己的作用域鏈也可以訪(fǎng)問(wèn)這些變量。而利用這一點(diǎn),就可以創(chuàng)建用于訪(fǎng)問(wèn)私有變量的公有方法。
我們把有權(quán)訪(fǎng)問(wèn)私有變量和私有函數(shù)的公有方法稱(chēng)為特權(quán)方法。有兩種在對(duì)象上創(chuàng)建特權(quán)方法的方式。第一種是在構(gòu)造函數(shù)中定義特權(quán)方法:
function MyObject() {
//私有變量和私有函數(shù)
var privateVariable = 10;
function privateFunction() {
return false;
}
//特權(quán)方法
this.publicMethod = function () {
privateVariable++;
return privateFunction();
};
}
在構(gòu)造函數(shù)中定義特權(quán)方法有一個(gè)缺點(diǎn),那就是你必須使用函數(shù)模式來(lái)達(dá)到這個(gè)目的。前面已經(jīng)討論過(guò),構(gòu)造函數(shù)模式的缺點(diǎn)是針對(duì)每個(gè)實(shí)例都會(huì)創(chuàng)建同樣一組新方法,而使用靜態(tài)私有變量來(lái)實(shí)現(xiàn)特權(quán)方法可以避免這個(gè)問(wèn)題。
靜態(tài)私有變量
通過(guò)在私有作用域中定義私有變量或函數(shù),同樣也可以創(chuàng)建特權(quán)方法:
(function () {
//私有變量和私有函數(shù)
var privateVariable = 10;
function privateFunction() {
return false;
}
//構(gòu)造函數(shù)
MyObject = function () {
};
//公有/特權(quán)方法
MyObject.prototype.publicMethod = function () {
privateVariable++;
return privateFunction();
};
})();
需要注意的是,這個(gè)模式在定義構(gòu)造函數(shù)時(shí)并沒(méi)有使用函數(shù)聲明,而是使用了函數(shù)表達(dá)式。函數(shù)聲明只能創(chuàng)建局部函數(shù),但那并不是我們想要的。出于同樣的原因,我們也沒(méi)有在聲明MyObject時(shí)使用var關(guān)鍵字。記?。撼跏蓟唇?jīng)聲明的變量,總是會(huì)創(chuàng)建一個(gè)全局變量。因此,MyObject就成了一個(gè)全局變量,能夠在私有作用域之外被訪(fǎng)問(wèn)到。但也要知道,在嚴(yán)格模式下給未經(jīng)聲明的變量賦值會(huì)導(dǎo)致錯(cuò)誤。
這個(gè)模式與在構(gòu)造函數(shù)中定義特權(quán)方法的主要區(qū)別,就在于私有變量和函數(shù)是由實(shí)例共享的。由于特權(quán)方法是在原型上定義的,因此所有實(shí)例都使用同一個(gè)函數(shù)。而這個(gè)特權(quán)方法,作為一個(gè)閉包,總是保存著對(duì)包含作用域的引用。
(function () {
var name = "";
Person = function (value) {
name = value;
};
Person.prototype.getName = function () {
return name;
};
Person.prototype.setName = function (value) {
name = value;
};
})();
var person1 = new Person("Nicholas");
alert(person1.getName()); //"Nicholas"
person1.setName("Greg");
alert(person1.getName()); //"Greg"
var person2 = new Person("Michael");
alert(person1.getName()); //"Michael"
alert(person2.getName()); //"Michael"
以這種方式創(chuàng)建靜態(tài)私有變量會(huì)因?yàn)槭褂迷投鲞M(jìn)代碼復(fù)用,但每個(gè)實(shí)例都沒(méi)有自己的私有變量。到底是使用實(shí)例變量,還是靜態(tài)私有變量,最終還是要視你的具體需求而定。
多查找作用域鏈中的一個(gè)層次,就會(huì)在一定程度上影響查找速度。而這正是使用閉包和私有變量的一個(gè)明顯的不足之處。
模塊模式
前面的模式是用于為自定義類(lèi)型創(chuàng)建私有變量和特權(quán)方法的。而道格拉斯所說(shuō)的模塊模式則是為單例創(chuàng)建私有變量和特權(quán)方法。所謂單例,指的就是只有一個(gè)實(shí)例的對(duì)象。按照慣例,JavaScript是以對(duì)象字面量的方式來(lái)創(chuàng)建單例對(duì)象的。
var singleton = {
name : value,
method : function () {
//這里是方法的代碼
}
};
模塊模式通過(guò)為單例添加私有變量和特權(quán)方法能夠使其得到增強(qiáng):
var singleton = function () {
//私有變量和私有函數(shù)
var privateVariable = 10;
function privateFunction() {
return false;
}
//特權(quán)/公有方法和屬性
return {
publicProperty: true,
publicMethod: function () {
privateVariable++;
return privateFunction();
}
};
}();
這個(gè)模塊模式使用了一個(gè)返回對(duì)象的匿名函數(shù)。在這個(gè)匿名函數(shù)內(nèi)部,首先定義了私有變量和函數(shù)。然后,將一個(gè)對(duì)象字面量作為函數(shù)的值返回。返回的對(duì)象字面量中只包含可以公開(kāi)的屬性和方法。由于這個(gè)對(duì)象是在匿名函數(shù)內(nèi)部定義的,因此它的公有方法有權(quán)訪(fǎng)問(wèn)私有變量和函數(shù)。從本質(zhì)上來(lái)講,這個(gè)對(duì)象字面量定義的是單例的公共接口。這種模式在需要對(duì)單例進(jìn)行某些初始化,同時(shí)又需要維護(hù)其私有變量時(shí)是非常有用的:
簡(jiǎn)言之,如果必須創(chuàng)建一個(gè)對(duì)象并以某些數(shù)據(jù)對(duì)其進(jìn)行初始化,同時(shí)還要公開(kāi)一些能夠訪(fǎng)問(wèn)這些私有數(shù)據(jù)的方法,那么就可以使用模塊模式。以這種模式創(chuàng)建的每個(gè)單例都是Object的實(shí)例,因?yàn)樽罱K要通過(guò)一個(gè)對(duì)象字面量來(lái)表示它。事實(shí)上,這也沒(méi)有什么;畢竟,單例通常都是作為全局對(duì)象存在的,我們不會(huì)將它傳遞給一個(gè)函數(shù)。因此,也就沒(méi)有什么必要使用instanceof操作符來(lái)檢查其對(duì)象類(lèi)型了。
增強(qiáng)的模塊模式
var application = function () {
//私有變量和函數(shù)
var components = new Array();
//初始化
components.push(new BaseComponent());
//公共
return {
getComponentCount: function () {
return components.length;
},
registerComponent: function (component) {
if (typeof component == "object") {
components.push(component);
}
}
};
}();
變?yōu)椋?/p>
var application = function () {
//私有變量和函數(shù)
var components = new Array();
//初始化
components.push(new BaseComponent());
//創(chuàng)建application的一個(gè)局部副本
var app = new BaseComponent();
//公共接口
app.getComponentCount = function () {
return components.length;
};
app.registerComponent = function (component) {
if (typeof component == "object") {
components.push(component);
}
};
//返回這個(gè)副本
return app;
}();
這個(gè)重寫(xiě)后的應(yīng)用程序(application)單例中,首先也是像前面例子中一樣定義了私有變量。主要的不同之處在于命名變量app的創(chuàng)建過(guò)程,因?yàn)樗仨毷荁aseComponent的實(shí)例。這個(gè)實(shí)例實(shí)際上是application對(duì)象的局部變量版。此后,我們又為app對(duì)象添加了能夠訪(fǎng)問(wèn)私有變量的公有方法。最后一步是返回app對(duì)象,結(jié)果仍然是將它賦值給全局變量application。