本章內(nèi)容
- 函數(shù)表達式的特征
- 使用函數(shù)實現(xiàn)遞歸
- 使用閉包定義私有變量
第五章曾介紹過臀脏,定義函數(shù)的方式有兩種:一種是函數(shù)聲明擦剑,另一種是函數(shù)表達式
關(guān)于函數(shù)聲明,它的一個重要特征是函數(shù)提升存谎,這就意味著可以把函數(shù)聲明放在調(diào)用它的語句后面返顺。
sayHi();//不會拋出錯誤。
function sayHi(){
alert("hi");
}
關(guān)于函數(shù)表達式站绪,它有幾種不同的語法形式遭铺。
var functionName = function(arg0, arg1, arg2){
//函數(shù)體
};
這種形式好像常規(guī)的變量賦值語句恢准,即創(chuàng)建一個函數(shù)并將它賦值給變量魂挂。functionName.這種情況下創(chuàng)建的函數(shù)叫做匿名函數(shù)。匿名函數(shù)的name屬性是空字符串馁筐。
函數(shù)表達式與其他表達式一樣涂召,在使用前必須先賦值。以下代碼會導致錯誤敏沉。
sayHi();//錯誤果正,函數(shù)還不存在
var sayHi = function(){
alert("hi");
}
理解函數(shù)提升的關(guān)鍵,就是理解函數(shù)聲明與函數(shù)表達式的區(qū)別盟迟。
能夠創(chuàng)建函數(shù)再賦值給變量秋泳,也能夠把函數(shù)作為其他函數(shù)的值返回。
一攒菠、遞歸
遞歸函數(shù)是指一個函數(shù)通過名字調(diào)用自身迫皱,如下是一個經(jīng)典的階乘函數(shù)。
function factorial(num){
if(num <= 1){
return 1;
} else {
return num * factorial(num -1);
}
}
//下面代碼可能導致階乘函數(shù)出錯
var anotherFactorial = factorial;
factorial = null;
alert(anotherFactorial (4));//出錯
上面出錯的原因是將factorial變量置為null辖众,結(jié)果指向原始函數(shù)的引用只剩下一個卓起。但在調(diào)用anotherFactorial 時,在函數(shù)內(nèi)部必須執(zhí)行factorial(num -1)凹炸,而factorial已不是函數(shù)戏阅,導致出錯。有兩種方法解決這個問題啤它。
//1.arguments.callee是指向正在執(zhí)行的函數(shù)的指針
function factorial(num){
if(num <= 1){
return 1;
} else {
return num * arguments.callee(num -1);
}
}
//2. 若在嚴格模式下饲握,不能通過腳本訪問arguments.callee私杜。使用命名函數(shù)表達式。
//就算將factorial賦值給另外一個變量救欧,函數(shù)f仍然有效。
var factorial = (function f(num){
if(num <= 1){
return 1;
} else {
return num * f(num -1)
})
二锣光、閉包
要區(qū)分匿名函數(shù)與閉包的區(qū)別笆怠。閉包是指有權(quán)訪問另一個函數(shù)作用域中變量的函數(shù)。創(chuàng)建閉包的常見方式誊爹,就是在一個函數(shù)內(nèi)部創(chuàng)建另一個函數(shù)蹬刷。如下:
function createComparionFunction(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;
}
};
}
當某個函數(shù)被調(diào)用時,會創(chuàng)建一個執(zhí)行環(huán)境及相應(yīng)的作用域鏈频丘。然后通過arguments和其他命名參數(shù)的值來初始化函數(shù)的活動對象办成。后臺的每個執(zhí)行環(huán)境都有一個表示變量的對象---變量對象。全局環(huán)境的變量對象始終存在搂漠,而局部環(huán)境的變量對象則只在函數(shù)執(zhí)行過程中存在迂卢。作用域鏈的本質(zhì)是一個指向變量對象的指針列表,它只引用但不包含實際的變量對象桐汤。
在上述例子中而克,閉包函數(shù)可以訪問外部函數(shù)中的propertyName。即使這個內(nèi)部函數(shù)被返回了怔毛,而且是在其他地方被調(diào)用了员萍,但它仍然可以訪問變量propertyName。之所以還能夠訪問這個變量是因為內(nèi)部函數(shù)的作用域鏈中包含createComparsionFunction()的作用域拣度。
在匿名函數(shù)從createComparisonFunction()中返回以后碎绎,它的作用域鏈被初始化為包含createComparsionFunction()函數(shù)的活動對象和全局變量對象。這樣匿名函數(shù)就可以訪問createComparsionFunction()中定義的變量抗果。更為重要的是createComparsionFunction()函數(shù)在執(zhí)行完畢之后筋帖,其活動對象也不會被銷毀,因為匿名函數(shù)的作用域鏈仍然在引用這個活動對象窖张。換句話說幕随,當createComparsionFunction()函數(shù)返回后,其執(zhí)行環(huán)境的作用域會被銷毀宿接,但它的活動對象仍然會留在內(nèi)存中赘淮,直到匿名函數(shù)被銷毀后,createComparsionFunction()函數(shù)的活動對象才會被銷毀睦霎。例如:
//創(chuàng)建函數(shù)
var compareNames = createComparsionFunction("name");
//調(diào)用函數(shù)
var result = compareNames({name:"Nicholas"},{name:"Greg"});
//解除對匿名函數(shù)的引用(以便釋放內(nèi)存)
comparNames = null;
2.1 閉包與變量
作用域鏈的這種配置機制梢卸,引出了一個值得注意的副作用,即閉包只能取得包含函數(shù)中任何變量的最后一個值副女。
function createFunctions(){
var result = new Array();
for(var i=0; i<10蛤高; i++){
result[i] = function(){
return i; //直接把閉包賦值給數(shù)組
};
}
return result;
}
上述每個函數(shù)都返回10,因為每個函數(shù)作用域鏈中都保存著createFunctions()的活動對象,所以他們引用的是同一個變量戴陡。但我們可以用以下匿名函數(shù)強制讓閉包的行為符合預(yù)期塞绿。
function createFunctions(){
var result = new Array();
for(var i = 0; i<10; i++){
result[i] = function(num){
return function (){ return num; }//按值傳遞num,定義一個匿名函數(shù)返回num
}(i);
}
return result;
}
在重寫了函數(shù)之后恤批,每個函數(shù)就會返回各自不同的索引值了异吻。在這個版本中,我們沒有直接把閉包賦值給數(shù)組喜庞,而是定義了一個匿名函數(shù)诀浪,并將立即執(zhí)行該匿名函數(shù)的結(jié)果賦值給數(shù)組延都。這個匿名函數(shù)有一個參數(shù)num雷猪,并且是按照值傳遞的,所以會將變量i的當前值賦值給num晰房。而在這個匿名函數(shù)內(nèi)部求摇,又創(chuàng)建并返回了一個訪問num的閉包。這樣result數(shù)組中都有自己的num的一個副本嫉你,因此就可以返回各自不同的值了月帝。
2.2 關(guān)于this對象
我們知道,this對象是在運行時基于函數(shù)的執(zhí)行環(huán)境綁定的:在全局函數(shù)中幽污,this等于window嚷辅,而當函數(shù)被作為某個對象的方法調(diào)用時,this等于哪個對象距误。不過匿名函數(shù)的執(zhí)行環(huán)境具有全局性簸搞,因此this通常指向window。
var name = "This window";
var object = {
name:"My Object",
getNameFunc:function(){
return funcion () {
return this.name;
}
}
};
alert(object.getNameFunc()());//"This window"
上述例子調(diào)用匿名函數(shù)并立即執(zhí)行准潭,為什么返回的是全局name變量的值而沒有取得其包含作用域(或外部作用域)的this對象呢趁俊?
每個函數(shù)在被調(diào)用時都會自動取得兩個特殊的變量:this和arguments。內(nèi)部函數(shù)在搜索這兩個變量時刑然,只會搜索到其活動對象為止寺擂,因此永遠不可能直接訪問外部函數(shù)中的這兩個變量。不過把外部作用域中的this對象保存在一個閉包能夠訪問的變量里泼掠,就可以讓閉包訪問該對象了怔软。如下所示
var name = "This window";
var object = {
name:"My object";
getNameFunc: function(){
var that = this;
return function(){
return that.name;
}
}
};
alert(object.getNameFunc()()); //"My object"
2.3 內(nèi)存泄漏
閉包有時會導致一些特殊的問題。具體來說择镇,如果閉包的作用域鏈中保存著一個HTML元素挡逼,那么就意味著該元素將無法被銷毀。
function assignHandler(){
var element = document.getElementById("someElement");
element.onclick = function(){
alert(element.id);
}
}
以上代碼創(chuàng)建了一個作為element元素事件處理程序的閉包腻豌,而這個閉包又創(chuàng)建了一個循環(huán)引用家坎。由于匿名函數(shù)中保存了一個對assignHandler()活動對象的引用嘱能,因此就導致無法減少element的引用數(shù)。只要匿名函數(shù)存在虱疏,element的引用數(shù)至少也是1惹骂,因此它的內(nèi)存將永遠無法被回收《┛颍可以通過稍微改寫一下代碼來解決析苫。
function assignHandler(){
var element = document.getElementById("someElement");
var id = element.id;
element.onclick = function(){
alert(id);
}
element = null;
}
通過把element.id的一個副本保存在變量中,并且在閉包中引用該變量消除了循環(huán)引用穿扳。但這并不足以解決內(nèi)存泄漏的問題。必須要記坠酢:閉包會引用包含函數(shù)的整個活動對象矛物,而其中包含著element。即使閉包不直接引用element跪但,包含函數(shù)的活動對象中也會保存著一個引用履羞。因此有必要把element變量置為null。
三屡久、模仿塊級作用域
Javascript中沒有塊級作用域的概念忆首。這意味著在塊語句中定義的變量爆价,實際上是在函數(shù)中而非語句中定義的汤锨。看下面的例子厌小。
function outputNumbers(count){
for(var i=0;i<count;i++){
alert(i);
}
var 0; //重新定義變量
alert(i); //計數(shù)
}
這個函數(shù)中定義了一個for循環(huán)筛欢,而變量i的初始值被設(shè)為0浸锨。在java/C++等語言中,變量i只會在for循環(huán)語句中有定義版姑,循環(huán)一旦結(jié)束柱搜,變量i就會被銷毀“眨可是在JavaScript中聪蘸,變量i是定義在outputNumbers()的活動對象中的。因此函數(shù)中可以隨處訪問這個變量表制。即使重新聲明健爬,Javascript也只會對后續(xù)的聲明視而不見。
用作塊級作用域(通常稱作私有作用域)的匿名函數(shù)語法如下所示:
(function(){
//這里是塊級作用域夫凸。
})();
將匿名函數(shù)的函數(shù)聲明包含在圓括號中表示它實際上是一個函數(shù)表達式浑劳。而緊隨其后的另一對圓括號表示立即調(diào)用這個函數(shù)。在匿名函數(shù)中定義的任何變量夭拌,都會在執(zhí)行結(jié)束時被銷毀魔熏。
無論在什么地方衷咽,只要臨時需要一些變量,就可以使用私有作用域蒜绽。這種技術(shù)經(jīng)常在全局作用域中被用在函數(shù)外部镶骗,從而限制向全局作用域中添加過多的變量和函數(shù)。通過創(chuàng)建私有作用域躲雅,每個開發(fā)人員既可以使用自己的變量鼎姊,又不必擔心搞亂全局作用域。
四相赁、私有變量
嚴格來講相寇,Javascript沒有私有成員的概念,所有的對象屬性都是公有的钮科。但有一個私有變量的概念唤衫。任何在函數(shù)中定義的變量,都可以認為是私有變量绵脯,因為不能在函數(shù)外部訪問這些變量佳励。私有變量包括函數(shù)的參數(shù)、局部變量和在函數(shù)中定義的其他函數(shù)蛆挫。
在函數(shù)內(nèi)部定義的私有變量和私有函數(shù)赃承,在函數(shù)外部不能訪問他們。如果在函數(shù)內(nèi)部創(chuàng)建一個閉包悴侵,那么閉包通過自己的作用域鏈可以訪問他們瞧剖。
我們把有權(quán)訪問私有變量和私有函數(shù)的方法稱為特權(quán)方法。有兩種在函數(shù)中創(chuàng)建特權(quán)函數(shù)的方法畜挨。第一種是在構(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ù)內(nèi)部定義了所有私有變量和函數(shù)。然后又繼續(xù)創(chuàng)建了能夠訪問這些私有成員的特權(quán)方法巴元。能夠在構(gòu)造函數(shù)中定義特權(quán)方法毡咏,是因為特權(quán)方法作為閉包有權(quán)訪問在構(gòu)造函數(shù)中定義的所有變量和函數(shù)。
除了利用特權(quán)方法訪問私有變量逮刨。利用私有和特權(quán)成員呕缭,還可以隱藏哪些不應(yīng)該被直接修改的數(shù)據(jù)。
在構(gòu)造函數(shù)中定義特權(quán)方法也有一個缺點修己,即每個實例都會創(chuàng)建同一組新方法恢总。使用靜態(tài)私有變量可以避免這個問題。
4.1 靜態(tài)私有變量
通過在私有作用域中定義私有變量或函數(shù)睬愤,同樣也可以創(chuàng)建特權(quán)方法片仿,其基本模式如下所示。
(function(){
//私有變量和私有函數(shù)
var privateVariable = 10;
function privateFunction(){
return false;
}
//構(gòu)造函數(shù),MyObject是一個全局變量
MyObject = function(){};
//公有/特權(quán)方法
MyObject.prototype.publichMethod = function(){
privateVariable++;
return privateFunction();
};
})();
這個模式創(chuàng)建了一個私有作用域尤辱,并在其中封裝了一個構(gòu)造函數(shù)及其相應(yīng)的方法砂豌。在私有作用域中首先定義了私有變量和私有函數(shù)厢岂,然后又定義了構(gòu)造函數(shù)和公有方法,公有方法是在原型上定義的阳距。在聲明MyObject時沒有使用var關(guān)鍵字塔粒。記住:初始化未經(jīng)聲明的變量筐摘,總是會創(chuàng)建一個全局變量卒茬。因此MyObject就成了一個全局變量,能夠在私有作用域之外被訪問到咖熟。
這個模式與在構(gòu)造函數(shù)中定義特權(quán)方法的主要區(qū)別在于:私有變量和函數(shù)是由實例共享的圃酵。特權(quán)方法是在原型上定義的,因此所有實例都使用同一個函數(shù)馍管。
看下面具體一個例子辜昵。
(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(person2.getName()); //"Michael"
alert(person2.getName()); //"Michael"
可以看出,在上述模式下咽斧,變量name變成了一個靜態(tài)的,由所有實例共享的屬性躬存。也就是說张惹,調(diào)用setName會影響所有實例。不過岭洲,到底是使用實例變量還是使用靜態(tài)私有變量需要視情況而定宛逗。
4.2 模塊模式
模塊模式是指為單例創(chuàng)建私有變量和特權(quán)方法。所謂單例盾剩,指的是只有一個實例的對象雷激。按照慣例,Javascript利用對象字面量的方式來創(chuàng)建對象.
模塊模式通過為單例添加私有變量和特權(quán)方法使其功能得到增強告私。
var singleton =function(){
//私有變量和私有函數(shù)
var privateVariable = 10;
function privateFunction(){
return false;
}
//特權(quán)/公有方法和屬性
return {
publicProperty:true,
publicMethod:function(){
privateVariable++;
return privateFunction();
}
};
}();
這個模塊模式使用了一個返回對象的匿名函數(shù)屎暇。在這個匿名函數(shù)內(nèi)部,首先定義了私有變量和函數(shù)驻粟。然后根悼,將一個對象字面量作為函數(shù)的值返回。返回的對象字面量中值包含可以公開的屬性和方法蜀撑。由于這個對象字面量是在匿名函數(shù)中定義的挤巡,因此它的公有方法有權(quán)訪問私有變量和函數(shù)。從本質(zhì)上講酷麦,這個對象字面量定義的是單例的公共接口矿卑。這種模式在需要對單例進行某些初始化,同時又需要維護其私有變量時是非常有用的沃饶。
4.3增強的模塊模式
這種增強的模塊模式適合那些單例必須是某種類型的實例母廷,同時還必須添加某些屬性和方法以對其實現(xiàn)增強的情況轻黑。如下application對象必須是BaseComponent類型的實例。
var application = function(){
//私有變量和私有函數(shù)
var components = new Array();
//初始化
components.push(new Basecomponent());
//創(chuàng)建一個application的局部副本
var app = new BaseComponent();
//公共接口
app.getComponentCount = function(){
return components.length;
}
app.registerComponents = function(component){
if(typeof component == "object"){
components.push(component)
}
};
//返回這個副本
return app;
}();
上述例子的不同之處在于命名變量app的創(chuàng)建過程徘意,因為它必須是BaseComponent的實例苔悦。