定義函數(shù)的方式有兩種:函數(shù)聲明和函數(shù)表達(dá)式却音。
// 函數(shù)聲明
function fn(){}
函數(shù)聲明的一個(gè)重要特征就是函數(shù)聲明提升,意思是在執(zhí)行代碼前會先讀取函數(shù)聲明淌喻。這就意味著可以把函數(shù)聲明放在調(diào)用它的語句后面僧家。
第二種創(chuàng)建函數(shù)的方式是使用函數(shù)表達(dá)式。這種情況下創(chuàng)建的函數(shù)叫匿名函數(shù)裸删。
var fn=function(){}
函數(shù)表達(dá)式與其他表達(dá)式一樣,在使用前必須先賦值阵赠。以下代碼會導(dǎo)致錯(cuò)誤涯塔。
sayHi(); //錯(cuò)誤:函數(shù)還不存在
var sayHi = function(){
alert("Hi!");
};
能夠創(chuàng)建函數(shù)再賦值給變量肌稻,也就能夠把函數(shù)作為其他函數(shù)的值返回。
function demo(propertyName) {
return function(object1, object2){
var value1 = object1[propertyName];
var value2 = object2[propertyName];
if (value1 < value2){
return -1;
} else if (value1 > value2){
return 1;
} else {
return 0;
}
};
}
demo()
就返回了一個(gè)匿名函數(shù)匕荸。返回的函數(shù)可能會被賦值給一個(gè)變量爹谭,或者以其他方式被調(diào)用;不過榛搔,在demo()
函數(shù)內(nèi)部诺凡,它是匿名的。在把函數(shù)當(dāng)成值來使用的情況下践惑,都可以使用匿名函數(shù)腹泌。不過,這并不是匿名函數(shù)唯一的用途尔觉。
遞歸
遞歸函數(shù)是在一個(gè)函數(shù)內(nèi)通過名字調(diào)用自身的情況下構(gòu)成的凉袱。
function factorial(num) {
if(num<=1){
return 1;
}else{
return num*factorial(num-1);
}
}
下面的代碼卻可能導(dǎo)致它出錯(cuò)。
var anotherFactorial = factorial ;
factorial = null;
alert(anotherFactorial(4)); // 出錯(cuò)
以上代碼先把factorial()
函數(shù)保存在變量anotherFactorial
中侦铜,然后將factorial
變量設(shè)置為null
专甩,結(jié)果指向原始函數(shù)的引用只剩下一個(gè)。但在接下來調(diào)用anotherFactorial()
時(shí)钉稍,由于必須執(zhí)行factorial()
涤躲,而factorial
已經(jīng)不再是函數(shù),所以就會導(dǎo)致錯(cuò)誤贡未。在這種情況下篓叶,使用arguments.callee
可以解決這個(gè)問題。
arguments.callee
是一個(gè)指向正在執(zhí)行的函數(shù)的指針羞秤,因此可以用它來實(shí)現(xiàn)對函數(shù)的遞歸調(diào)用缸托。
function factorial(num) {
if(num <= 1) {
return 1;
} else {
return num*arguments.callee(num-1);
}
}
通過使用arguments.callee
代替函數(shù)名,可以確保無論怎樣調(diào)用函數(shù)都不會出問題瘾蛋。因此俐镐,在編寫遞歸函數(shù)時(shí),使用arguments.callee
總比使用函數(shù)名更保險(xiǎn)哺哼。
但在嚴(yán)格模式下佩抹,不能通過腳本訪問arguments.callee
。訪問這個(gè)屬性會導(dǎo)致錯(cuò)誤取董。不過棍苹,可以使用命名函數(shù)表達(dá)式來達(dá)成相同的結(jié)果。
var factorial = (function f (num) {
if (num <= 1 ) {
return 1;
} else {
return num*f(num-1);
}
});
閉包
閉包是指有權(quán)訪問另一個(gè)函數(shù)作用域中的變量的函數(shù)茵汰。創(chuàng)建閉包的常見方式就是在一個(gè)函數(shù)內(nèi)部創(chuàng)建另一個(gè)函數(shù)枢里。
function demo(propertyName) {
return function(object1, object2){
var value1 = object1[propertyName];
var value2 = object2[propertyName];
if (value1 < value2){
return -1;
} else if (value1 > value2){
return 1;
} else {
return 0;
}
};
}
當(dāng)某個(gè)函數(shù)被調(diào)用時(shí),會創(chuàng)建一個(gè)執(zhí)行環(huán)境及相應(yīng)的作用域鏈。然后栏豺,使用arguments
和其他命名參數(shù)的值來初始化函數(shù)的活動(dòng)對象彬碱。但在作用域鏈中,外部函數(shù)的活動(dòng)對象始終處于第二位奥洼,外部函數(shù)的外部函數(shù)的活動(dòng)對象處于第三位巷疼,.....直至作為作用域鏈終點(diǎn)的全局執(zhí)行環(huán)境。
在函數(shù)執(zhí)行過程中灵奖,為讀取和寫入變量的值嚼沿,就需要在作用域鏈中查找變量。
function compare(value1, value2){
if (value1 < value2){
return -1;
} else if (value1 > value2){
return 1;
} else {
return 0;
}
}
var result = compare(5, 10);
當(dāng)調(diào)用compare()
時(shí)瓷患,會創(chuàng)建一個(gè)包含arguments
骡尽、value1
和value2
的活動(dòng)對象。全局執(zhí)行環(huán)境的變量對象(包含result
和compare
)在compare()
執(zhí)行環(huán)境的作用域鏈中則處于第二位尉尾。
后臺的每個(gè)執(zhí)行環(huán)境都有一個(gè)表示變量的對象——變量對象爆阶。全局環(huán)境的變量對象始終存在,而像
compare()
函數(shù)這樣的局部環(huán)境的變量對象沙咏,則只在函數(shù)執(zhí)行的過程中存在辨图。在創(chuàng)建compare()
函數(shù)時(shí),會創(chuàng)建一個(gè)預(yù)先包含全局變量對象的作用域鏈肢藐,這個(gè)作用域鏈被保存在內(nèi)部的[[Scope]]
屬性中故河。 當(dāng)調(diào)用compare()
函數(shù)時(shí),會為函數(shù)創(chuàng)建一個(gè)執(zhí)行環(huán)境吆豹,然后通過復(fù)制函數(shù)的[[Scope]]
屬性中的對象構(gòu)建起執(zhí)行環(huán)境的作用域鏈鱼的。此后,又有一個(gè)活動(dòng)對象(在此作為變量對象使用)被創(chuàng)建并被推入執(zhí)行環(huán)境作用域鏈的前端痘煤。對于這個(gè)例子中compare()
函數(shù)的執(zhí)行環(huán)境而言凑阶,其作用域鏈中包含兩個(gè)變量對象:本地活動(dòng)對象和全局變量對象。顯然衷快,作用域鏈本質(zhì)上是一個(gè)指向變量對象的指針列表宙橱,它只引用但不實(shí)際包含變量對象。無論什么時(shí)候在函數(shù)中訪問一個(gè)變量時(shí)蘸拔,就會從作用域鏈中搜索具有相應(yīng)名字的變量师郑。一般來講, 當(dāng)函數(shù)執(zhí)行完畢后调窍,局部活動(dòng)對象就會被銷毀宝冕,內(nèi)存中僅保存全局作用域(全局執(zhí)行環(huán)境的變量對象)。但是邓萨,閉包的情況又有所不同地梨。
在另一個(gè)函數(shù)內(nèi)部定義的函數(shù)會將包含函數(shù)(即外部函數(shù))的活動(dòng)對象添加到它的作用域鏈中菊卷。因此,在
demo()
函數(shù)內(nèi)部定義的匿名函數(shù)的作用域鏈中湿刽,實(shí)際上將會包含外部函數(shù) demo()
的活動(dòng)對象的烁。
var compare = demo("name");
var result = compare({ name: "Nicholas" }, { name: "Greg" });
在匿名函數(shù)從demo()
中被返回后褐耳,它的作用域鏈被初始化為包含demo()
函數(shù)的活動(dòng)對象和全局變量對象诈闺。這樣,匿名函數(shù)就可以訪問在demo()
中定義的所有變量铃芦。更為重要的是雅镊,demo()
函數(shù)在執(zhí)行完畢后,其活動(dòng)對象也不會被銷毀刃滓,因?yàn)槟涿瘮?shù)的作用域鏈仍然在引用這個(gè)活動(dòng)對象仁烹。換句話說,當(dāng)demo()
函數(shù)返回后咧虎,其執(zhí)行環(huán)境的作用域鏈會被銷毀卓缰,但它的活 動(dòng)對象仍然會留在內(nèi)存中;直到匿名函數(shù)被銷毀后征唬,demo()
的活動(dòng)對象才會被銷毀。
//創(chuàng)建函數(shù)
var compareNames = demo("name");
//調(diào)用函數(shù)
var result = compareNames({ name: "Nicholas" }, { name: "Greg" });
//解除對匿名函數(shù)的引用(以便釋放內(nèi)存)
compareNames = null;
首先茁彭,創(chuàng)建的比較函數(shù)被保存在變量compareNames
中总寒。而通過將compareNames
設(shè)置為等于null
解除該函數(shù)的引用,就等于通知垃圾回收例程將其清除理肺。隨著匿名函數(shù)的作用域鏈被銷毀摄闸,其他作用域 (除了全局作用域)也都可以安全地銷毀了。
由于閉包會攜帶包含它的函數(shù)的作用域妹萨,因此會比其他函數(shù)占用更多的內(nèi)存年枕。過度使用閉包可能會導(dǎo)致內(nèi)存占用過多。
閉包與變量
閉包只能取得包含函數(shù)中任何變量的最后一個(gè)值乎完。閉包所保存的是整個(gè)變量對象熏兄,而不是某個(gè)特殊的變量。
function createFunctions(){
var result = new Array();
for (var i=0; i < 10; i++){
result[i] = function(){
return i;
};
}
return result;
}
這個(gè)函數(shù)會返回一個(gè)函數(shù)數(shù)組囱怕。表面上看霍弹,似乎每個(gè)函數(shù)都應(yīng)該返自己的索引值,即位置0的函數(shù)返回0娃弓,位置1的函數(shù)返回1典格,以此類推。但實(shí)際上台丛,每個(gè)函數(shù)都返回10耍缴。因?yàn)槊總€(gè)函數(shù)的作用域鏈中都保存createFunctions()
函數(shù)的活動(dòng)對象 砾肺, 所以它們引用的都是同一個(gè)變量i
。 當(dāng)createFunctions()
函數(shù)返回后防嗡,變量i
的值是10变汪,此時(shí)每個(gè)函數(shù)都引用著保存變量i
的同一個(gè)變量 對象,所以在每個(gè)函數(shù)內(nèi)部i
的值都是10蚁趁。但是裙盾,我們可以通過創(chuàng)建另一個(gè)匿名函數(shù)強(qiáng)制讓閉包的行為符合預(yù)期。
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()
函數(shù)后他嫡,每個(gè)函數(shù)就會返回各自不同的索引值了番官。在這個(gè)版本中,我們沒有直接把閉包賦值給數(shù)組钢属,而是定義了一個(gè)匿名函數(shù)徘熔,并將立即執(zhí)行該匿名函數(shù)的結(jié)果賦給數(shù)組。這里的匿名函數(shù)有一個(gè)參數(shù)num
淆党,也就是最終的函數(shù)要返回的值酷师。在調(diào)用每個(gè)匿名函數(shù)時(shí),我們傳入了變量i
染乌。由于函數(shù)參數(shù)是按值傳遞的山孔,所以就會將變量 i 的當(dāng)前值復(fù)制給參數(shù)num
。而在這個(gè)匿名函數(shù)內(nèi)部慕匠,又創(chuàng)建并返回了一個(gè)訪問num
的閉包饱须。這樣一來,result
數(shù)組中的每個(gè)函數(shù)都有自己num
變量的一個(gè)副本台谊,因此就可以返回各自不同的數(shù)值了蓉媳。
關(guān)于this對象
在閉包中使用this
對象也可能會導(dǎo)致一些問題。我們知道锅铅,this
對象是在運(yùn)行時(shí)基于函數(shù)的執(zhí) 行環(huán)境綁定的:在全局函數(shù)中酪呻,this
等于window
,而當(dāng)函數(shù)被作為某個(gè)對象的方法調(diào)用時(shí)盐须,this
等于那個(gè)對象玩荠。不過,匿名函數(shù)的執(zhí)行環(huán)境具有全局性贼邓,因此其this
對象通常指向window
阶冈。但有時(shí)候由于編寫閉包的方式不同,這一點(diǎn)可能不會那么明顯塑径。
var name = "The Window";
var object = {
name : "My Object",
getNameFunc : function(){
return function(){
return this.name;
};
}
};
alert(object.getNameFunc()()); //"The Window"(在非嚴(yán)格模式下)
每個(gè)函數(shù)在被調(diào)用時(shí)都會自動(dòng)取得兩個(gè)特殊變量:this
和arguments
女坑。內(nèi)部函數(shù)在搜索這兩個(gè)變量時(shí),只會搜索到其活動(dòng)對象為止统舀,因此永遠(yuǎn)不可能直接訪問外部函數(shù)中的這兩個(gè)變量匆骗。不過劳景,把外部作用域中的this
對象保存在一個(gè)閉包能夠訪問到的變量里,就可以讓閉包訪問該對象了碉就。
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
和arguments
也存在同樣的問題盟广。如果想訪問作用域中的arguments
對象,必須將對該對象的引用保存到另一個(gè)閉包能夠訪問的變量中瓮钥。
在幾種特殊情況下筋量,this
的值可能會意外地改變。
var name = "The Window";
var object = {
name : "My Object",
getName: function(){
return this.name;
}
};
這里的getName()
方法只簡單地返回this.name
的值骏庸。以下是幾種調(diào)用object.getName()
的方式以及各自的結(jié)果毛甲。
object.getName(); //"My Object"
(object.getName)(); //"My Object"
(object.getName = object.getName)(); //"The Window"年叮,在非嚴(yán)格模式下
內(nèi)存泄漏
由于IE9之前的版本對JScript
對象和COM
對象使用不同的垃圾收集例程具被,因此閉包在IE的這些版本中會導(dǎo)致一些特殊的問題。具體來說只损,如果閉包的作用域鏈中保存著一個(gè)HTML元素一姿,那么就意味著該元素將無法被銷毀。
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è)對assignHandler()
的活動(dòng)對象的引用,因此就會導(dǎo)致無法減少element
的引用數(shù)爆存。只要匿名函數(shù)存在蛉顽,element
的引用數(shù)至少也是1,因此它所占用的內(nèi)存就永遠(yuǎn)不會被回收先较。不過携冤,這個(gè)問題可以通過稍微改寫一下代碼來解決。
function assignHandler(){
var element = document.getElementById("someElement");
var id = element.id;
element.onclick = function(){
alert(id);
};
element = null;
}
在上面的代碼中闲勺,通過把element.id
的一個(gè)副本保存在一個(gè)變量中曾棕,并且在閉包中引用該變量消除了循環(huán)引用。但僅僅做到這一步菜循,還是不能解決內(nèi)存泄漏的問題翘地。必須要記住:閉包會引用包含函數(shù)的整個(gè)活動(dòng)對象癌幕,而其中包含著element
衙耕。即使閉包不直接引用element
,包含函數(shù)的活動(dòng)對象中也仍然會保存一個(gè)引用勺远。因此橙喘,有必要把element
變量設(shè)置為null
。這樣就能夠解除對DOM對象的引用谚中,順利地減少其引用數(shù)渴杆,確保正沉戎Γ回收其占用的內(nèi)存。
模仿塊級作用域
JS沒有塊級作用域的概念磁奖。這意味著在塊語句中定義的變量囊拜,實(shí)際上是在包含函數(shù)中而非語句中創(chuàng)建的。
functìon outputNumbers(count){
for (var i=0;i<count;i++){
alert(i) ;
}
alert(i); //計(jì)數(shù)
}
在JS中比搭,變量i
是定義在ouputNumbers()
的活動(dòng)對象中的冠跷,因此從它有定義開始,就可以在函數(shù)內(nèi)部隨處訪問它身诺。即使像下面這樣錯(cuò)誤地重新聲明同一個(gè)變量蜜托,也不會改變它的值。
function outputNumbers(count){
for (var i =0;i<count;i++) {
alert(i);
}
var i; //重新聲明變量
alert(i) ; // 計(jì)數(shù)
}
JS從來不會告訴你是否多次聲明了同一個(gè)變量霉赡;遇到這種情況橄务,它只會對后續(xù)的聲明視而不見(不過,它會執(zhí)行后續(xù)聲明中的變量初始化)穴亏。匿名函數(shù)可以用來模仿塊級作用域并避免這個(gè)問題蜂挪。
用作塊級作用域(通常稱為私有作用域)的匿名函數(shù)的語法如下所示钟沛。
(function(){
//這里是塊級作用域
})();
以上代碼定義并立即調(diào)用了一個(gè)匿名函數(shù)榆骚。將函數(shù)聲明包含在一對圓括號中,表示它實(shí)際上是一個(gè)函數(shù)表達(dá)式比吭。而緊隨其后的另一對圓括號會立即調(diào)用這個(gè)函數(shù)刺覆。
無論在什么地方严肪,只要臨時(shí)需要一些變量,就可以使用私有作用域谦屑,例如:
function outputNumbers(count){
(function () {
for (var i=0; i < count; i++){
alert(i);
}
})();
alert(i); //導(dǎo)致一個(gè)錯(cuò)誤驳糯!
}
這種做法可以減少閉包占用的內(nèi)存問題,因?yàn)闆]有指向匿名函數(shù)的引用伦仍。只要名函數(shù)執(zhí)行完畢结窘,就可以立即銷段其作用域鏈了。
私有變量
嚴(yán)格來講充蓝,JS中沒有私有成員的概念隧枫;所有對象屬性都是公有的。不過谓苟,倒是有一個(gè)私有變量的概念官脓。任何在函數(shù)中定義的變量,都可以認(rèn)為是私有變量涝焙,因?yàn)椴荒茉诤瘮?shù)的外部訪問這些變量卑笨。私有變量包括函數(shù)的參數(shù)、局部變量和在函數(shù)內(nèi)部定義的其他函數(shù)仑撞。
function add (num1 , num2 ) {
var sum = num1 + num2;
return sum;
}
在這個(gè)函數(shù)內(nèi)部有3個(gè)私有變量:num1
赤兴,num2
和sum
妖滔。在函數(shù)內(nèi)部可以訪問這幾個(gè)變量,但在函數(shù)外部則不能訪問它們桶良。如果在這個(gè)函數(shù)內(nèi)部創(chuàng)建一個(gè)閉包座舍,那么閉包通過自己的作用域鏈也可以訪問這些變量。而利用這一點(diǎn)陨帆,就可以創(chuàng)建用于訪問私有變量的公有方法曲秉。
我們把有權(quán)訪問私有變量和私有函數(shù)的公有方法稱為特權(quán)方法。有兩種在對象上創(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è)模式在構(gòu)造函數(shù)內(nèi)部定義了所有私有變量和函數(shù)亥鸠。然后,又繼續(xù)創(chuàng)建了能夠訪問這些私有成員的特權(quán)方法缩焦。能夠在構(gòu)造函數(shù)重定義特權(quán)方法读虏,是因?yàn)樘貦?quán)方法作為閉包有權(quán)訪問在構(gòu)造函數(shù)中定義的所有變量和函數(shù)。對這個(gè)例子而言袁滥,變量privateVariable
和函數(shù)privateFunction()
只能通過特權(quán)方法publicMethod()
來訪問。在創(chuàng)建MyObject
的實(shí)例后灾螃,除了使用publicMethod()
這一個(gè)途徑外题翻,沒有任何辦法可以直接訪問privateVariable
和privateFunction()
。
利用私有和特權(quán)成員腰鬼, 可以隱藏那些不應(yīng)該被直接修改的數(shù)據(jù):
function Person(name){
this.getName = function(){
return name;
};
this.setName = function (value) {
name = value;
};
}
var person = new Person("Nicholas");
alert(person.getName()); //"Nicholas"
person.setName("Greg");
alert(person.getName()); //"Greg"
在構(gòu)造函數(shù)中定義特權(quán)方法也有一個(gè)缺點(diǎn)嵌赠,那就是你必須使用構(gòu)造函數(shù)模式來達(dá)到這個(gè)目的。構(gòu)造函數(shù)模式的缺點(diǎn)是針對每個(gè)實(shí)例都會創(chuàng)建同樣一組新方法熄赡,而使用靜態(tài)私有變量來實(shí)現(xiàn)特權(quán)方法就可以避免這個(gè)問題姜挺。
靜態(tài)私有變量
通過在私有作用域中定義私有變量或函數(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è)模式創(chuàng)建了一個(gè)私有作用域炊豪,并在其中封裝了一個(gè)構(gòu)造函數(shù)及相應(yīng)的方法。在私有作用域中拧篮, 首先定義了私有變量和私有函數(shù)词渤,然后又定義了構(gòu)造函數(shù)及其公有方法。公有方法是在原型上定義的串绩, 這一點(diǎn)體現(xiàn)了典型的原型模式缺虐。需要注意的是,這個(gè)模式在定義構(gòu)造函數(shù)時(shí)并沒有使用函數(shù)聲明礁凡,而是 使用了函數(shù)表達(dá)式高氮。函數(shù)聲明只能創(chuàng)建局部函數(shù)慧妄,但那并不是我們想要的。出于同樣的原因剪芍,我們也沒 有在聲明MyObject
時(shí)使用var
關(guān)鍵字腰涧。記住:初始化未經(jīng)聲明的變量紊浩,總是會創(chuàng)建一個(gè)全局變量窖铡。 因此,MyObject
就成了一個(gè)全局變量坊谁,能夠在私有作用域之外被訪問到费彼。但也要知道,在嚴(yán)格模式下給未經(jīng)聲明的變量賦值會導(dǎo)致錯(cuò)誤口芍。
這個(gè)模式與在構(gòu)造函數(shù)中定義特權(quán)方法的主要區(qū)別箍铲,就在于私有變量和函數(shù)是由實(shí)例共享的。由于特權(quán)方法是在原型上定義的鬓椭,因此所有實(shí)例都使用同一個(gè)函數(shù)颠猴。而這個(gè)特權(quán)方法,作為一個(gè)閉包小染,總是保存著對包含作用域的引用翘瓮。
(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"
這個(gè)例子中的Person
構(gòu)造函數(shù)與getName()
和setName()
方法一樣,都有權(quán)訪問私有變量name
裤翩。
在這種模式下资盅,變量name
就變成了一個(gè)靜態(tài)的、由所有實(shí)例共享的屬性踊赠。也就是說呵扛,在一個(gè)實(shí)例上調(diào)用setName()
會影響所有實(shí)例。而調(diào)用setName()
或新建一個(gè)Person
實(shí)例都會賦予name
屬性一個(gè)新值筐带。結(jié)果就是所有實(shí)例都會返回相同的值今穿。
以這種方式創(chuàng)建靜態(tài)私有變量會因?yàn)槭褂迷投鲞M(jìn)代碼復(fù)用,但每個(gè)實(shí)例都沒有自己的私有變量伦籍。到底是使用實(shí)例變量蓝晒,還是靜態(tài)私有變量,最終還是要視你的具體需求而定鸽斟。
多查找作用域鏈中的一個(gè)層次拔创,就會在一定程度上影響查找速度。而這正是使用閉包和私有變量的一個(gè)顯明的不足之處富蓄。
模塊模式
模塊模式是為單例創(chuàng)建私有變量和特權(quán)方法剩燥。所謂單例,指的就是只有一個(gè)實(shí)例的對象。 按照慣例灭红,JS是以對象字面量的方式來創(chuàng)建單例對象的侣滩。
var singleton = {
name : value,
method : function () {
//這里是方法的代碼
}
};
模塊模式通過為單例添加私有變量和特權(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è)返回對象的匿名函數(shù)变擒。在這個(gè)匿名函數(shù)內(nèi)部君珠,首先定義了私有變量和函數(shù)。 然后娇斑,將一個(gè)對象字面量作為函數(shù)的值返回策添。返回的對象字面量中只包含可以公開的屬性和方法。由于這個(gè)對象是在匿名函數(shù)內(nèi)部定義的毫缆,因此它的公有方法有權(quán)訪問私有變量和函數(shù)唯竹。從本質(zhì)上來講,這個(gè)對象字面量定義的是單例的公共接口苦丁。這種模式在需要對單例進(jìn)行某些初始化浸颓,同時(shí)又需要維護(hù)其私有變量時(shí)是非常有用的。
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);
}
}
};
}();
在 Web 應(yīng)用程序中旺拉,經(jīng)常需要使用一個(gè)單例來管理應(yīng)用程序級的信息产上。這個(gè)簡單的例子創(chuàng)建了一個(gè)用于管理組件的application
對象。在創(chuàng)建這個(gè)對象的過程中蛾狗,首先聲明了一個(gè)私有的components
數(shù)組晋涣,并向數(shù)組中添加了一個(gè)BaseComponent
的新實(shí)例(在這里不需要關(guān)心BaseComponent
的代碼,我們只是用它來展示初始化操作)淘太。而返回對象的getComponentCount()
和registerComponent()
方法姻僧,都是有權(quán)訪問數(shù)組components
的特權(quán)方法。前者只是返回已注冊的組件數(shù)目蒲牧,后者用于注冊新組件。
簡言之赌莺,如果必須創(chuàng)建一個(gè)對象并以某些數(shù)據(jù)對其進(jìn)行初始化冰抢,同時(shí)還要公開一些能夠訪問這些私有數(shù)據(jù)的方法,那么就可以使用模塊模式艘狭。以這種模式創(chuàng)建的每個(gè)單例都是Object的實(shí)例挎扰,因?yàn)樽罱K要通過一個(gè)對象字面量來表示它。事實(shí)上巢音,這也沒有什么遵倦;畢竟,單例通常都是作為全局對象存在的官撼,我們不會將它傳遞給一個(gè)函數(shù)梧躺。因此,也就沒有什么必要使用instanceof
操作符來檢查其對象類型了。
增強(qiáng)的模塊模式
在返回對象之前加入對其增強(qiáng)的代碼掠哥。這種增強(qiáng)的模塊模式適合那些單例必須是某種類型的實(shí)例巩踏,同時(shí)還必須添加某些屬性和方法對其加以增強(qiáng)的情況。
var singleton = function(){
//私有變量和私有函數(shù)
var privateVariable = 10;
function privateFunction(){
return false;
}
//創(chuàng)建對象
var object = new CustomType();
//添加特權(quán)/公有屬性和方法
object.publicProperty = true;
object.publicMethod = function(){
privateVariable++;
return privateFunction();
};
//返回這個(gè)對象
return object;
}();
如果前面演示模塊模式的例子中的application
對象必須是BaseComponent
的實(shí)例续搀,那么就可以使用以下代碼塞琼。
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è)重寫后的應(yīng)用程序(application)單例中,首先也是像前面例子中一樣定義了私有變量禁舷。主要的不同之處在于命名變量app
的創(chuàng)建過程彪杉,因?yàn)樗仨毷?code>BaseComponent的實(shí)例。這個(gè)實(shí)例實(shí)際上是application
對象的局部變量版牵咙。此后派近,我們又為app
對象添加了能夠訪問私有變量的公有方法。 最后一步是返回app
對象霜大,結(jié)果仍然是將它賦值給全局變量application
构哺。
小結(jié)
在JS編程中,函數(shù)表達(dá)式是一種非常有用的技術(shù)战坤。使用函數(shù)表達(dá)式可以無須對函數(shù)命名曙强, 從而實(shí)現(xiàn)動(dòng)態(tài)編程。匿名函數(shù)途茫,也稱為拉姆達(dá)函數(shù)碟嘴,是一種使用JS函數(shù)的強(qiáng)大方式。以下總結(jié)了函數(shù)表達(dá)式的特點(diǎn)囊卜。
- 函數(shù)表達(dá)式不同于函數(shù)聲明娜扇。函數(shù)聲明要求有名字,但函數(shù)表達(dá)式不需要栅组。沒有名字的函數(shù)表達(dá)式也叫做匿名函數(shù)雀瓢。
- 在無法確定如何引用函數(shù)的情況下,遞歸函數(shù)就會變得比較復(fù)雜玉掸;
- 遞歸函數(shù)應(yīng)該始終使用
arguments.callee
來遞歸地調(diào)用自身刃麸,不要使用函數(shù)名,函數(shù)名可能會發(fā)生變化司浪。
當(dāng)在函數(shù)內(nèi)部定義了其他函數(shù)時(shí)泊业,就創(chuàng)建了閉包。閉包有權(quán)訪問包含函數(shù)內(nèi)部的所有變量啊易。
- 在后臺執(zhí)行環(huán)境中吁伺,閉包的作用域鏈包含著它自己的作用域、包含函數(shù)的作用域和全局作用域租谈。
- 通常篮奄,函數(shù)的作用域及其所有變量都會在函數(shù)執(zhí)行結(jié)束后被銷毀。
- 但是,當(dāng)函數(shù)返回了一個(gè)閉包時(shí)宦搬,這個(gè)函數(shù)的作用域?qū)恢痹趦?nèi)存中保存到閉包不存在為止牙瓢。
使用閉包可以在JS中模仿塊級作用域(JS本身沒有塊級作用域的概念)。
- 創(chuàng)建并立即調(diào)用一個(gè)函數(shù)间校,這樣既可以執(zhí)行其中的代碼矾克,又不會在內(nèi)存中留下對該函數(shù)的引用。
- 結(jié)果就是函數(shù)內(nèi)部的所有變量都會被立即銷毀——除非將某些變量賦值給了包含作用域(即外部作用域)中的變量憔足。
閉包還可以用于在對象中創(chuàng)建私有變量胁附。
- 即使JS中沒有正式的私有對象屬性的概念,但可以使用閉包來實(shí)現(xiàn)公有方法滓彰,而通過公有方法可以訪問在包含作用域中定義的變量控妻。
- 有權(quán)訪問私有變量的公有方法叫做特權(quán)方法。
- 可以使用構(gòu)造函數(shù)模式揭绑、原型模式來實(shí)現(xiàn)自定義類型的特權(quán)方法弓候,也可以使用模塊模式、增強(qiáng)的模塊模式來實(shí)現(xiàn)單例的特權(quán)方法他匪。
JS中的函數(shù)表達(dá)式和閉包都是極其有用的特性菇存,利用它們可以實(shí)現(xiàn)很多功能。不過邦蜜,因?yàn)閯?chuàng)建閉包必須維護(hù)額外的作用域依鸥,所以過度使用它們可能會占用大量內(nèi)存。