函數(shù)表達(dá)式是 JavaScript中的一個(gè)既強(qiáng)大又容易令人困惑的特性趴拧。第 5章曾介紹過婆誓,定義函數(shù)的 方式有兩種:一種是函數(shù)聲明,另一種就是函數(shù)表達(dá)式捞蛋。函數(shù)聲明的語法是這樣的。
function functionName(arg0, arg1, arg2) {
//函數(shù)體
}
關(guān)于函數(shù)聲明柬姚,它的一個(gè)重要特征就是函數(shù)聲明提升(function declaration hoisting)拟杉,意思是在執(zhí)行 代碼之前會(huì)先讀取函數(shù)聲明。這就意味著可以把函數(shù)聲明放在調(diào)用它的語句后面量承。
第二種創(chuàng)建函數(shù)的方式是使用函數(shù)表達(dá)式搬设。函數(shù)表達(dá)式有幾種不同的語法形式。下面是常見的一 種形式撕捍。
var functionName = function(arg0, arg1, arg2){
//函數(shù)體
};
這種形式看起來好像是常規(guī)的變量賦值語句拿穴,即創(chuàng)建一個(gè)函數(shù)并將它賦值給變量 functionName。 這種情況下創(chuàng)建的函數(shù)叫做匿名函數(shù)(anonymous function)忧风,因?yàn)?function 關(guān)鍵字后面沒有標(biāo)識(shí)符贞言。 (匿名函數(shù)有時(shí)候也叫拉姆達(dá)函數(shù)。)匿名函數(shù)的 name 屬性是空字符串阀蒂。 函數(shù)表達(dá)式與其他表達(dá)式一樣,在使用前必須先賦值弟蚀。
遞歸
遞歸函數(shù)是在一個(gè)函數(shù)通過名字調(diào)用自身的情況下構(gòu)成的蚤霞,如下所示。
function factorial(num){
if (num <= 1){
return 1;
} else {
return num * factorial(num-1);
}
}
我們知道义钉,arguments.callee 是一個(gè)指向正在執(zhí)行的函數(shù)的指針昧绣,因此可以用它來實(shí)現(xiàn)對(duì)函數(shù) 的遞歸調(diào)用,例如:
function factorial(num){
if (num <= 1){
return 1;
} else {
return num * arguments.callee(num-1);
}
}
加粗的代碼顯示捶闸,通過使用 arguments.callee 代替函數(shù)名夜畴,可以確保無論怎樣調(diào)用函數(shù)都不會(huì) 出問題。因此删壮,在編寫遞歸函數(shù)時(shí)贪绘,使用 arguments.callee 總比使用函數(shù)名更保險(xiǎn)。
閉包
有不少開發(fā)人員總是搞不清匿名函數(shù)和閉包這兩個(gè)概念央碟,因此經(jīng)乘肮啵混用。閉包是指有權(quán)訪問另一個(gè) 函數(shù)作用域中的變量的函數(shù)亿虽。創(chuàng)建閉包的常見方式菱涤,就是在一個(gè)函數(shù)內(nèi)部創(chuàng)建另一個(gè)函數(shù),仍以前面的 createComparisonFunction()函數(shù)為例洛勉,注意加粗的代碼粘秆。
function createComparisonFunction(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;
}
};
}
在這個(gè)例子中,突出的那兩行代碼是內(nèi)部函數(shù)(一個(gè)匿名函數(shù))中的代碼收毫,這兩行代碼訪問了外部 函數(shù)中的變量 propertyName攻走。即使這個(gè)內(nèi)部函數(shù)被返回了殷勘,而且是在其他地方被調(diào)用了,但它仍然可 以訪問變量 propertyName陋气。之所以還能夠訪問這個(gè)變量劳吠,是因?yàn)閮?nèi)部函數(shù)的作用域鏈中包含 createComparisonFunction()的作用域。要徹底搞清楚其中的細(xì)節(jié)巩趁,必須從理解函數(shù)被調(diào)用的時(shí)候 都會(huì)發(fā)生什么入手痒玩。 第 4章介紹了作用域鏈的概念。而有關(guān)如何創(chuàng)建作用域鏈以及作用域鏈有什么作用的細(xì)節(jié)议慰,對(duì)徹底 理解閉包至關(guān)重要蠢古。當(dāng)某個(gè)函數(shù)被調(diào)用時(shí),會(huì)創(chuàng)建一個(gè)執(zhí)行環(huán)境(execution context)及相應(yīng)的作用域鏈别凹。 然后草讶,使用 arguments 和其他命名參數(shù)的值來初始化函數(shù)的活動(dòng)對(duì)象(activation object)。但在作用域 鏈中炉菲,外部函數(shù)的活動(dòng)對(duì)象始終處于第二位堕战,外部函數(shù)的外部函數(shù)的活動(dòng)對(duì)象處于第三位,……直至作為作用域鏈終點(diǎn)的全局執(zhí)行環(huán)境拍霜。
閉包與變量
作用域鏈的這種配置機(jī)制引出了一個(gè)值得注意的副作用嘱丢,即閉包只能取得包含函數(shù)中任何變量的 后一個(gè)值。別忘了閉包所保存的是整個(gè)變量對(duì)象祠饺,而不是某個(gè)特殊的變量越驻。
關(guān)于this對(duì)象
在閉包中使用 this 對(duì)象也可能會(huì)導(dǎo)致一些問題。我們知道道偷,this 對(duì)象是在運(yùn)行時(shí)基于函數(shù)的執(zhí) 行環(huán)境綁定的:在全局函數(shù)中缀旁,this 等于 window,而當(dāng)函數(shù)被作為某個(gè)對(duì)象的方法調(diào)用時(shí)勺鸦,this 等 于那個(gè)對(duì)象并巍。不過,匿名函數(shù)的執(zhí)行環(huán)境具有全局性换途,因此其 this 對(duì)象通常指向 window①履澳。但有時(shí)候 由于編寫閉包的方式不同,這一點(diǎn)可能不會(huì)那么明顯怀跛。
內(nèi)存泄漏
由于 IE9之前的版本對(duì) JScript對(duì)象和 COM對(duì)象使用不同的垃圾收集例程(第 4章曾經(jīng)討論過)距贷,
因此閉包在 IE 的這些版本中會(huì)導(dǎo)致一些特殊的問題。具體來說吻谋,如果閉包的作用域鏈中保存著一個(gè) HTML元素忠蝗,那么就意味著該元素將無法被銷毀。
模仿塊級(jí)作用域
JavaScript從來不會(huì)告訴你是否多次聲明了同一個(gè)變量漓拾;遇到這種情況阁最,它只會(huì)對(duì)后續(xù)的聲明視而不 見(不過戒祠,它會(huì)執(zhí)行后續(xù)聲明中的變量初始化)。匿名函數(shù)可以用來模仿塊級(jí)作用域并避免這個(gè)問題速种。 用作塊級(jí)作用域(通常稱為私有作用域)的匿名函數(shù)的語法如下所示姜盈。
(function(){ //這里是塊級(jí)作用域 })();
以上代碼定義并立即調(diào)用了一個(gè)匿名函數(shù)。將函數(shù)聲明包含在一對(duì)圓括號(hào)中配阵,表示它實(shí)際上是一個(gè) 函數(shù)表達(dá)式馏颂。而緊隨其后的另一對(duì)圓括號(hào)會(huì)立即調(diào)用這個(gè)函數(shù)。
私有變量
嚴(yán)格來講棋傍,JavaScript 中沒有私有成員的概念救拉;所有對(duì)象屬性都是公有的。不過瘫拣,倒是有一個(gè)私有 變量的概念亿絮。任何在函數(shù)中定義的變量,都可以認(rèn)為是私有變量麸拄,因?yàn)椴荒茉诤瘮?shù)的外部訪問這些變量派昧。 私有變量包括函數(shù)的參數(shù)、局部變量和在函數(shù)內(nèi)部定義的其他函數(shù)拢切。
我們把有權(quán)訪問私有變量和私有函數(shù)的公有方法稱為特權(quán)方法(privileged method)斗锭。有兩種在對(duì)象 上創(chuàng)建特權(quán)方法的方式。第一種是在構(gòu)造函數(shù)中定義特權(quán)方法
這個(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ù)。
靜態(tài)私有變量
通過在私有作用域中定義私有變量或函數(shù)烈疚,同樣也可以創(chuàng)建特權(quán)方法
這個(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)聲明的變量已脓,總是會(huì)創(chuàng)建一個(gè)全局變量珊楼。 因此,MyObject 就成了一個(gè)全局變量度液,能夠在私有作用域之外被訪問到厕宗。但也要知道,在嚴(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ì)包含作用域的引用
模塊模式
前面的模式是用于為自定義類型創(chuàng)建私有變量和特權(quán)方法的兢仰。而道格拉斯所說的模塊模式(module pattern)則是為單例創(chuàng)建私有變量和特權(quán)方法。所謂單例(singleton)剂碴,指的就是只有一個(gè)實(shí)例的對(duì)象把将。 按照慣例,JavaScript是以對(duì)象字面量的方式來創(chuàng)建單例對(duì)象的忆矛。
這個(gè)模塊模式使用了一個(gè)返回對(duì)象的匿名函數(shù)察蹲。在這個(gè)匿名函數(shù)內(nèi)部,首先定義了私有變量和函數(shù)催训。 然后洽议,將一個(gè)對(duì)象字面量作為函數(shù)的值返回。返回的對(duì)象字面量中只包含可以公開的屬性和方法漫拭。由于 這個(gè)對(duì)象是在匿名函數(shù)內(nèi)部定義的亚兄,因此它的公有方法有權(quán)訪問私有變量和函數(shù)。從本質(zhì)上來講采驻,這個(gè) 對(duì)象字面量定義的是單例的公共接口审胚。這種模式在需要對(duì)單例進(jìn)行某些初始化,同時(shí)又需要維護(hù)其私有 變量時(shí)是非常有用的
簡(jiǎn)言之礼旅,如果必須創(chuàng)建一個(gè)對(duì)象并以某些數(shù)據(jù)對(duì)其進(jìn)行初始化膳叨,同時(shí)還要公開一些能夠訪問這些私有 數(shù)據(jù)的方法,那么就可以使用模塊模式痘系。以這種模式創(chuàng)建的每個(gè)單例都是 Object 的實(shí)例懒鉴,因?yàn)榻K要通 過一個(gè)對(duì)象字面量來表示它。事實(shí)上,這也沒有什么临谱;畢竟璃俗,單例通常都是作為全局對(duì)象存在的,我們不 會(huì)將它傳遞給一個(gè)函數(shù)悉默。因此城豁,也就沒有什么必要使用 instanceof 操作符來檢查其對(duì)象類型了。
增強(qiáng)的模塊模式
有人進(jìn)一步改進(jìn)了模塊模式抄课,即在返回對(duì)象之前加入對(duì)其增強(qiáng)的代碼唱星。這種增強(qiáng)的模塊模式適合那 些單例必須是某種類型的實(shí)例,同時(shí)還必須添加某些屬性和(或)方法對(duì)其加以增強(qiáng)的情況跟磨。
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; }();
ModuleAugmentationPatternExample01.htm 在這個(gè)重寫后的應(yīng)用程序(application)單例中间聊,首先也是像前面例子中一樣定義了私有變量。主 要的不同之處在于命名變量 app 的創(chuàng)建過程抵拘,因?yàn)樗仨毷?BaseComponent 的實(shí)例哎榴。這個(gè)實(shí)例實(shí)際上 是 application 對(duì)象的局部變量版。此后僵蛛,我們又為 app 對(duì)象添加了能夠訪問私有變量的公有方法尚蝌。 后一步是返回 app 對(duì)象,結(jié)果仍然是將它賦值給全局變量 application充尉。