第七章 函數表達式
定義函數的方式有兩種:一種是函數聲明,另一種就是函數表達式。函數聲明的語法是這樣的。
function functionName(arg0, arg1, arg2) { //函數體 }
首先是function 關鍵字,然後是函數的名字,這就是指定函數名的方式。Firefox、Safari、Chrome和Opera 都給函數定義了一個非標準的name 屬性,通過這個屬性可以訪問到給函數指定的名字。這個屬性的值永遠等於跟在function 關鍵字後面的標識符。
//只在Firefox、Safari、Chrome 和Opera 有效 alert(functionName.name); //"functionName"
關於函數聲明,它的一個重要特征就是函數聲明提升(function declaration hoisting),意思是在執行代碼之前會先讀取函數聲明。這就意味著可以把函數聲明放在調用它的語句後面。
sayHi(); function sayHi(){ alert("Hi!"); }
這個例子不會拋出錯誤,因為在代碼執行之前會先讀取函數聲明。
第二種創建函數的方式是使用函數表達式。函數表達式有幾種不同的語法形式。下面是最常見的一種形式。
var functionName = function(arg0, arg1, arg2){ //函數體 };
這種形式看起來好像是常規的變量賦值語句,即創建一個函數並將它賦值給變量functionName。這種情況下創建的函數叫做匿名函數(anonymous function)
函數表達式與其他表達式一樣,在使用前必須先賦值。以下代碼會導致錯誤。
sayHi(); //錯誤:函數還不存在 var sayHi = function(){ alert("Hi!"); };
理解函數提升的關鍵,就是理解函數聲明與函數表達式之間的區別。例如,執行以下代碼的結果可能會讓人意想不到。
//不要這樣做! if(condition){ function sayHi(){ alert("Hi!"); } } else { functionsayHi(){ alert("Yo!"); } }
表面上看,以上代碼表示在condition 為true 時,使用一個sayHi()的定義;否則,就使用另一個定義。實際上,這在ECMAScript 中屬於無效語法,JavaScript 引擎會嘗試修正錯誤,將其轉換為合理的狀態。但問題是瀏覽器嘗試修正錯誤的做法並不一致。大多數瀏覽器會返回第二個聲明,忽略condition;Firefox 會在condition 為true 時返回第一個聲明。因此這種使用方式很危險,不應該出現在你的代碼中。不過,如果是使用函數表達式,那就沒有什麽問題了。
//可以這樣做 var sayHi; if(condition){ sayHi = function(){ alert("Hi!"); }; } else { sayHi = function(){ alert("Yo!"); }; }
這個例子不會有什麽意外,不同的函數會根據condition 被賦值給sayHi。
能夠創建函數再賦值給變量,也就能夠把函數作為其他函數的值返回。createComparisonFunction()函數:
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; } }; } var data=[{name:"Lily",age:28},{name:"Nicho",age:21}]; data.sort(createComparisonFunction(‘name‘)); console.log(data[0].name);//Lily data.sort(createComparisonFunction(‘age‘)); console.log(data[0].age);//21
createComparisonFunction()就返回了一個匿名函數。返回的函數可能會被賦值給一個變量,或者以其他方式被調用;不過,在createComparisonFunction()函數內部,它是匿名的。在把函數當成值來使用的情況下,都可以使用匿名函數
7.1 遞歸
遞歸函數是在一個函數通過名字調用自身的情況下構成的,如下
function factorial(num){ if(num<=1){ return 1; }else{ return num*factorial(num-1) } } console.log(factorial(4));//24
雖然這個函數表面看來沒什麽問題,但下面的代碼卻可能導致它出錯。
var anotherFactorial = factorial; factorial = null; alert(anotherFactorial(4)); //出錯!
以上代碼先把factorial()函數保存在變量anotherFactorial 中,然後將factorial 變量設置為null,結果指向原始函數的引用只剩下一個。但在接下來調用anotherFactorial()時,由於必須執行factorial(),而factorial 已經不再是函數,所以就會導致錯誤。在這種情況下,使用 arguments.callee 可以解決這個問題。
arguments.callee 是一個指向正在執行的函數的指針,因此可以用它來實現對函數的遞歸調用
function factorial(num){ if (num <= 1){ return 1; } else { return num * arguments.callee(num-1); } }
通過使用arguments.callee 代替函數名,可以確保無論怎樣調用函數都不會出問題。因此,在編寫遞歸函數時,使用arguments.callee 總比使用函數名更保險。
但在嚴格模式下,不能通過腳本訪問arguments.callee,訪問這個屬性會導致錯誤。不過,可以使用命名函數表達式來達成相同的結果。
var factorial=(function f(num){ if(num<=1){ return 1; }else{ return num*f(num-1); } }); console.log(factorial(4));//24 var anotherFactorial=factorial; factorial=null; console.log(anotherFactorial(4));//24
以上代碼創建了一個名為f()的命名函數表達式,然後將它賦值給變量factorial。即便把函數賦值給了另一個變量,函數的名字f 仍然有效,所以遞歸調用照樣能正確完成。這種方式在嚴格模式和非嚴格模式下都行得通。
7.2 閉包
閉包是指有權訪問另一個函數作用域中的變量的函數。創建閉包的常見方式,就是在一個函數內部創建另一個函數,如下
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; } }; }
在這個例子中,突出的那兩行代碼是內部函數(一個匿名函數)中的代碼,這兩行代碼訪問了外部函數中的變量propertyName。即使這個內部函數被返回了,而且是在其他地方被調用了,但它仍然可以訪問變量propertyName。之所以還能夠訪問這個變量,是因為內部函數的作用域鏈中包含createComparisonFunction()的作用域。
當某個函數被調用時,會創建一個執行環境(execution context)及相應的作用域鏈。然後,使用arguments 和其他命名參數的值來初始化函數的活動對象(activation object)。但在作用域鏈中,外部函數的活動對象始終處於第二位,外部函數的外部函數的活動對象處於第三位,……直至作為作用域鏈終點的全局執行環境。
在函數執行過程中,為讀取和寫入變量的值,就需要在作用域鏈中查找變量。
function compare(value1,value2){ if(value1<value2){ return -1; }else if(value1>value2){ return 1; }else{ return 0; } } var result=compare(5,10); console.log(result);//-1
以上代碼先定義了compare()函數,然後又在全局作用域中調用了它。當調用compare()時,會創建一個包含arguments、value1 和value2 的活動對象。全局執行環境的變量對象(包含result和compare)在compare()執行環境的作用域鏈中則處於第二位。圖7-1 展示了包含上述關系的compare()函數執行時的作用域鏈。
後臺的每個執行環境都有一個表示變量的對象——變量對象。全局環境的變量對象始終存在,而像compare()函數這樣的局部環境的變量對象,則只在函數執行的過程中存在。在創建compare()函數時,會創建一個預先包含全局變量對象的作用域鏈,這個作用域鏈被保存在內部的[[Scope]]屬性中。當調用compare()函數時,會為函數創建一個執行環境,然後通過復制函數的[[Scope]]屬性中的對象構建起執行環境的作用域鏈。此後,又有一個活動對象(在此作為變量對象使用)被創建並被推入執行環境作用域鏈的前端。對於這個例子中compare()函數的執行環境而言,其作用域鏈中包含兩個變量對象:本地活動對象和全局變量對象。顯然,作用域鏈本質上是一個指向變量對象的指針列表,它只引用但不實際包含變量對象。
無論什麽時候在函數中訪問一個變量時,就會從作用域鏈中搜索具有相應名字的變量。一般來講,當函數執行完畢後,局部活動對象就會被銷毀,內存中僅保存全局作用域(全局執行環境的變量對象)。但是,閉包的情況又有所不同。
在另一個函數內部定義的函數會將包含函數(即外部函數)的活動對象添加到它的作用域鏈中。因此,在createComparisonFunction()函數內部定義的匿名函數的作用域鏈中,實際上將會包含外部函數createComparisonFunction()的活動對象。
var compare = createComparisonFunction("name"); var result = compare({ name: "Nicholas" }, { name: "Greg" });
在匿名函數從createComparisonFunction()中被返回後,它的作用域鏈被初始化為包含createComparisonFunction()函數的活動對象和全局變量對象。這樣,匿名函數就可以訪問在createComparisonFunction()中定義的所有變量。更為重要的是,createComparisonFunction()函數在執行完畢後,其活動對象也不會被銷毀,因為匿名函數的作用域鏈仍然在引用這個活動對象。換
句話說,當createComparisonFunction()函數返回後,其執行環境的作用域鏈會被銷毀,但它的活動對象仍然會留在內存中;直到匿名函數被銷毀後,createComparisonFunction()的活動對象才會被銷毀
//創建函數 var compareNames = createComparisonFunction("name"); //調用函數 var result = compareNames({ name: "Nicholas" }, { name: "Greg" }); //解除對匿名函數的引用(以便釋放內存) compareNames = null;
首先,創建的比較函數被保存在變量compareNames 中。而通過將compareNames 設置為等於null解除該函數的引用,就等於通知垃圾回收例程將其清除。隨著匿名函數的作用域鏈被銷毀,其他作用域(除了全局作用域)也都可以安全地銷毀了。圖7-2 展示了調用compareNames()的過程中產生的作用域鏈之間的關系。
7.2.1 閉包與變量
作用域鏈的這種配置機制引出了一個值得註意的副作用,即閉包只能取得包含函數中任何變量的最後一個值。別忘了閉包所保存的是整個變量對象,而不是某個特殊的變量。
function createFunction(){ var result=new Array(); for(var i=0;i<10;i++){ result[i]=function(){ return i; }; } return result; } console.log(createFunction()[0]());//10 console.log(createFunction()[1]());//10 console.log(createFunction()[2]());//10 console.log(createFunction()[3]());//10
這個函數會返回一個函數數組。表面上看,似乎每個函數都應該返自己的索引值,即位置0 的函數返回0,位置1 的函數返回1,以此類推。但實際上,每個函數都返回10。因為每個函數的作用域鏈中都保存著createFunctions() 函數的活動對象, 所以它們引用的都是同一個變量i 。當createFunctions()函數返回後,變量i 的值是10,此時每個函數都引用著保存變量i 的同一個變量對象,所以在每個函數內部i 的值都是10。但是,我們可以通過創建另一個匿名函數強制讓閉包的行為符合預期,如下所示。
function createFunction(){ var result=new Array(); for(var i=0;i<10;i++){ result[i]=function(num){ return function(){ return num; }; }(i); } return result; } console.log(createFunction()[0]());//0 console.log(createFunction()[1]());//1 console.log(createFunction()[2]());//2 console.log(createFunction()[3]());//3
我們沒有直接把閉包賦值給數組,而是定義了一個匿名函數,並將立即執行該匿名函數的結果賦給數組。這裏的匿名函數有一個參數num,也就是最終的函數要返回的值。在調用每個匿名函數時,我們傳入了變量i。由於函數參數是按值傳遞的,所以就會將變量i 的當前值復制給參數num。而在這個匿名函數內部,又創建並返回了一個訪問num 的閉包。這樣一來,result 數組中的每個函數都有自己num 變量的一個副本,因此就可以返回各自不同的數值了。
function foo(x){ var tmp=3; return function(y){ console.log(x+y+(++tmp)); } } var bar=foo(2); bar(10);//16 bar(10);//17 bar(10);//18 bar(10);//19
由於tmp仍存在於bar閉包的內部,所以它還是會自加1,而且你每次調用bar時它都會自加1.
上面的x是一個字面值(值傳遞),和JS裏其他的字面值一樣,當調用foo時,實參x的值被復制了一份,復制的那一份作為了foo的參數x。那麽問題來了,JS裏處理object時是用到引用傳遞的,那麽,你調用foo時傳遞一個object,foo函數return的閉包也會引用最初那個object!
function foo(x){ var tmp=3; return function(y){ console.log(x+y+tmp); x.memb=x.memb?x.memb+1:1; console.log(x.memb); } } var age=new Number(2); var bar=foo(age); bar(10);//15 1 bar(10);//15 2 bar(10);//15 3
每次運行bar(10),x.memb都會自加1。但需要註意的是x每次都指向同一個object變量——age,運行兩次bar(10)後,age.memb會變成2
閉包經常用於創建含有隱藏數據的函數(但並不總是這樣)。
var db=(function(){ var data={}; return function(key,val){ if(val==undefined){ return data[key] }else{ return data[key]=val } } })(); console.log(db(‘x‘));//undefined console.log(db(‘x‘,1));//1 console.log(db(‘x‘));//1
7.2.2 關於this對象
在閉包中使用this 對象也可能會導致一些問題。我們知道,this 對象是在運行時基於函數的執行環境綁定的:在全局函數中,this 等於window,而當函數被作為某個對象的方法調用時,this 等於那個對象。不過,匿名函數的執行環境具有全局性,因此其this 對象通常指向window。但有時候由於編寫閉包的方式不同,這一點可能不會那麽明顯。
var name=‘The Window‘; var object={ name:‘My object‘, getNameFunc:function(){ return function(){ return this.name; } } }; console.log(object.getNameFunc()());//The Window (在非嚴格模式下)
每個函數在被調用時都會自動取得兩個特殊變量:this 和arguments。內部函數在搜索這兩個變量時,只會搜索到其活動對象為止,因此永遠不可能直接訪問外部函數中的這兩個變量。不過,把外部作用域中的this 對象保存在一個閉包能夠訪問到的變量裏,就可以讓閉包訪問該對象了,如下
var name="The window"; var object={ name:"My object", getNameFunc:function(){ var that=this; return function(){ return that.name; }; } }; console.log(object.getNameFunc()());//My object
在定義匿名函數之前,我們把this對象賦值給了一個名叫that 的變量。而在定義了閉包之後,閉包也可以訪問這個變量,因為它是我們在包含函數中特意聲名的一個變量。即使在函數返回之後,that 也仍然引用著object,所以調用object.getNameFunc()()就返回了"My Object"。
在幾種特殊情況下,this 的值可能會意外地改變。比如,下面的代碼是修改前面例子的結果。
var name="The window"; var object={ name:"My object", getName:function(){ return this.name; } };
這裏的getName()方法只簡單地返回this.name 的值。以下是幾種調用object.getName()的方式以及各自的結果。
object.getName(); //"My Object" (object.getName)(); //"My Object" (object.getName = object.getName)(); //"The Window",在非嚴格模式下
第三行代碼先執行了一條賦值語句,然後再調用賦值後的結果。因為這個賦值表達式的值是函數本身,所以this 的值不能得到維持,結果就返回了"The Window"。
7.2.3 內存泄漏
閉包在IE 的這些版本中會導致一些特殊的問題。具體來說,如果閉包的作用域鏈中保存著一個HTML 元素,那麽就意味著該元素將無法被銷毀。
function assignHandler(){ var element=document.getElementById(‘someElem‘); element.onclick=function(){ console.log(element.id);//someElem } } assignHandler();
以上代碼創建了一個作為element 元素事件處理程序的閉包,而這個閉包則又創建了一個循環引用。由於匿名函數保存了一個對assignHandler()的活動對象的引用,因此就會導致無法減少element 的引用數。只要匿名函數存在,element 的引用數至少也是1,因此它所占用的內存就永遠不會被回收。不過,這個問題可以通過稍微改寫一下代碼來解決
function assignHandler(){ var element=document.getElementById(‘someElem‘); var id=element.id; element.onclick=function(){ console.log(id); }; element=null; }
通過把element.id 的一個副本保存在一個變量中,並且在閉包中引用該變量消除了循環引用。但僅僅做到這一步,還是不能解決內存泄漏的問題。必須要記住:閉包會引用包含函數的整個活動對象,而其中包含著element。即使閉包不直接引用element,包含函數的活動對象中也仍然會保存一個引用。因此,有必要把element 變量設置為null。這樣就能夠解除對DOM 對象的引用,順利地減少其引用數,確保正常回收其占用的內存。
7.3 模仿塊級作用域
JavaScript 沒有塊級作用域的概念。這意味著在塊語句中定義的變量,實際上是在包含函數中而非語句中創建的
function outputNumbers(count){ for(var i=0;i<count;i++){ console.log(i);//0 1 2 3 4 } console.log(i);//5 } outputNumbers(5);
在JavaScrip 中,變量i是定義在ouputNumbers()的活動對象中的,因此從它有定義開始,就可以在函數內部隨處訪問它。即使像下面這樣錯誤地重新聲明同一個變量,也不會改變它的值。
function outputNumbers(count){ for(var i=0;i<count;i++){ console.log(i); } var i; //重新聲明變量 console.log(i); //計數 }
JavaScript 從來不會告訴你是否多次聲明了同一個變量;遇到這種情況,它只會對後續的聲明視而不見(不過,它會執行後續聲明中的變量初始化)。匿名函數可以用來模仿塊級作用域並避免這個問題。
用作塊級作用域(通常稱為私有作用域)的匿名函數的語法如下所示。
(function(){ //這裏是塊級作用域 })();
以上代碼定義並立即調用了一個匿名函數。將函數聲明包含在一對圓括號中,表示它實際上是一個函數表達式。而緊隨其後的另一對圓括號會立即調用這個函數。
這樣做之所以可行,是因為變量只不過是值的另一種表現形式,因此用實際的值替換變量沒有問題。
var someFunction = function(){ //這裏是塊級作用域 }; someFunction();
這個例子先定義了一個函數,然後立即調用了它。定義函數的方式是創建一個匿名函數,並把匿名函數賦值給變量someFunction。而調用函數的方式是在函數名稱後面添加一對圓括號,即someFunction()。通過前面的例子我們知道,可以使用實際的值來取代變量count,那在這裏是不是也可以用函數的值直接取代函數名呢? 然而,下面的代碼卻會導致錯誤。
function(){ //這裏是塊級作用域 }(); //出錯!
這段代碼會導致語法錯誤,是因為JavaScript 將function 關鍵字當作一個函數聲明的開始,而函數聲明後面不能跟圓括號。然而,函數表達式的後面可以跟圓括號。要將函數聲明轉換成函數表達式,只要像下面這樣給它加上一對圓括號即可。
(function(){ //這裏是塊級作用域 })();
無論在什麽地方,只要臨時需要一些變量,就可以使用私有作用域
function outputNumbers(count){ (function(){ for(var i=0;i<count;i++){ console.log(i); } })(); console.log(i);//導致一個錯誤 Uncaught ReferenceError: i is not defined } outputNumbers(5);
我們在for 循環外部插入了一個私有作用域。在匿名函數中定義的任何變量,都會在執行結束時被銷毀。因此,變量i 只能在循環中使用,使用後即被銷毀。而在私有作用域中能夠訪問變量count,是因為這個匿名函數是一個閉包,它能夠訪問包含作用域中的所有變量。
一般來說,我們都應該盡量少向全局作用域中添加變量和函數。在一個由很多開發人員共同參與的大型應用程序中,過多的全局變量和函數很容易導致命名沖突。而通過創建私有作用域,每個開發人員既可以使用自己的變量,又不必擔心搞亂全局作用域。
(function(){ var now=new Date(); if(now.getMoth==0 && now.getDate()==1){ console.log("Happy new year!"); }else{ console.log("Happy everday!"); } })();
7.4 私有變量
嚴格來講,JavaScript 中沒有私有成員的概念;所有對象屬性都是公有的。不過,倒是有一個私有變量的概念。任何在函數中定義的變量,都可以認為是私有變量,因為不能在函數的外部訪問這些變量。私有變量包括函數的參數、局部變量和在函數內部定義的其他函數。
function add(num1,num2){ var sum=num1+num2; return sum; }
在這個函數內部,有3 個私有變量:num1、num2 和sum。在函數內部可以訪問這幾個變量,但在函數外部則不能訪問它們。如果在這個函數內部創建一個閉包,那麽閉包通過自己的作用域鏈也可以訪問這些變量。而利用這一點,就可以創建用於訪問私有變量的公有方法。
我們把有權訪問私有變量和私有函數的公有方法稱為特權方法(privileged method)。有兩種在對象上創建特權方法的方式。第一種是在構造函數中定義特權方法,基本模式如下。
function MyObject(){ //私有變量和私有函數 var privateVariable = 10; function privateFunction(){ return false; } //特權方法 this.publicMethod = function (){ privateVariable++; return privateFunction(); }; }
這個模式在構造函數內部定義了所有私有變量和函數。然後,又繼續創建了能夠訪問這些私有成員的特權方法。能夠在構造函數中定義特權方法,是因為特權方法作為閉包有權訪問在構造函數中定義的所有變量和函數。對這個例子而言,變量privateVariable 和函數privateFunction()只能通過特權方法publicMethod()來訪問。在創建MyObject 的實例後,除了使用publicMethod()這一個途徑外,沒有任何辦法可以直接訪問privateVariable 和privateFunction()。
利用私有和特權成員,可以隱藏那些不應該被直接修改的數據,例如
function Person(name){ this.getName=function(){ return name; }; this.setName=function(value){ name=value; }; } var person=new Person("Nicho"); console.log(person.getName());//Nicho person.setName("Greg"); console.log(person.getName());//Greg
以上代碼的構造函數中定義了兩個特權方法:getName()和setName()。這兩個方法都可以在構造函數外部使用,而且都有權訪問私有變量name。但在Person 構造函數外部,沒有任何辦法訪問name。由於這兩個方法是在構造函數內部定義的,它們作為閉包能夠通過作用域鏈訪問name。私有變量name在Person 的每一個實例中都不相同,因為每次調用構造函數都會重新創建這兩個方法。不過,在構造函數中定義特權方法也有一個缺點,那就是你必須使用構造函數模式來達到這個目的。構造函數模式的缺點是針對每個實例都會創建同樣一組新方法,而使用靜態私有變量來實現特權方法就可以避免這個問題。
7.4.1 靜態私有變量
通過在私有作用域中定義私有變量或函數,同樣也可以創建特權方法,其基本模式如下所示。
(function(){ //私有變量和私有函數 var privateVariable = 10; function privateFunction(){ return false; } //構造函數 MyObject = function(){ }; //公有/特權方法 MyObject.prototype.publicMethod = function(){ privateVariable++; return privateFunction(); }; })();
這個模式創建了一個私有作用域,並在其中封裝了一個構造函數及相應的方法。在私有作用域中,首先定義了私有變量和私有函數,然後又定義了構造函數及其公有方法。公有方法是在原型上定義的,這一點體現了典型的原型模式。需要註意的是,這個模式在定義構造函數時並沒有使用函數聲明,而是使用了函數表達式。函數聲明只能創建局部函數,但那並不是我們想要的。出於同樣的原因,我們也沒有在聲明MyObject 時使用var 關鍵字。記住:初始化未經聲明的變量,總是會創建一個全局變量。因此,MyObject 就成了一個全局變量,能夠在私有作用域之外被訪問到。但也要知道,在嚴格模式下給未經聲明的變量賦值會導致錯誤。
這個模式與在構造函數中定義特權方法的主要區別,就在於私有變量和函數是由實例共享的。由於特權方法是在原型上定義的,因此所有實例都使用同一個函數。而這個特權方法,作為一個閉包,總是保存著對包含作用域的引用。
(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("Nicho"); console.log(person1.getName());//Nicho person1.setName("Greg"); console.log(person1.getName());//Greg var person2=new Person("Michael"); console.log(person2.getName());//Michael console.log(person1.getName());//Michael
這個例子中的Person 構造函數與getName()和setName()方法一樣,都有權訪問私有變量name。在這種模式下,變量name 就變成了一個靜態的、由所有實例共享的屬性。也就是說,在一個實例上調用setName()會影響所有實例。而調用setName()或新建一個Person 實例都會賦予name 屬性一個新值。結果就是所有實例都會返回相同的值。
以這種方式創建靜態私有變量會因為使用原型而增進代碼復用,但每個實例都沒有自己的私有變量。到底是使用實例變量,還是靜態私有變量,最終還是要視你的具體需求而定。
7.4.2 模塊模式
前面的模式是用於為自定義類型創建私有變量和特權方法的。而道格拉斯所說的模塊模式(module pattern)則是為單例創建私有變量和特權方法。所謂單例(singleton),指的就是只有一個實例的對象。按照慣例,JavaScript 是以對象字面量的方式來創建單例對象的。
var singleton = { name : value, method : function () { //這裏是方法的代碼 } };
模塊模式通過為單例添加私有變量和特權方法能夠使其得到增強,其語法形式如下:
var singleton = function(){ //私有變量和私有函數 var privateVariable = 10; function privateFunction(){ return false; } //特權/公有方法和屬性 return { publicProperty: true, publicMethod : function(){ privateVariable++; return privateFunction(); } }; }();
這個模塊模式使用了一個返回對象的匿名函數。在這個匿名函數內部,首先定義了私有變量和函數。然後,將一個對象字面量作為函數的值返回。返回的對象字面量中只包含可以公開的屬性和方法。由於這個對象是在匿名函數內部定義的,因此它的公有方法有權訪問私有變量和函數。從本質上來講,這個對象字面量定義的是單例的公共接口。這種模式在需要對單例進行某些初始化,同時又需要維護其私有變量時是非常有用的,例如
var application = function(){ //私有變量和函數 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 應用程序中,經常需要使用一個單例來管理應用程序級的信息。這個簡單的例子創建了一個用於管理組件的application 對象。在創建這個對象的過程中,首先聲明了一個私有的components數組,並向數組中添加了一個BaseComponent 的新實例(在這裏不需要關心BaseComponent 的代碼,我們只是用它來展示初始化操作)。而返回對象的getComponentCount()和registerComponent()方法,都是有權訪問數組components 的特權方法。前者只是返回已註冊的組件數目,後者用於註冊新組件。
簡言之,如果必須創建一個對象並以某些數據對其進行初始化,同時還要公開一些能夠訪問這些私有數據的方法,那麽就可以使用模塊模式。以這種模式創建的每個單例都是Object 的實例,因為最終要通過一個對象字面量來表示它。事實上,這也沒有什麽;畢竟,單例通常都是作為全局對象存在的,我們不會將它傳遞給一個函數。因此,也就沒有什麽必要使用instanceof 操作符來檢查其對象類型了。
7.4.3 增強的模塊模式
改進的模塊模式,即在返回對象之前加入對其增強的代碼。這種增強的模塊模式適合那些單例必須是某種類型的實例,同時還必須添加某些屬性和(或)方法對其加以增強的情況。
var singleton = function(){ //私有變量和私有函數 var privateVariable = 10; function privateFunction(){ return false; } //創建對象 var object = new CustomType(); //添加特權/公有屬性和方法 object.publicProperty = true; object.publicMethod = function(){ privateVariable++; return privateFunction(); }; //返回這個對象 return object; }();
如果前面演示模塊模式的例子中的application 對象必須是BaseComponent 的實例,那麽就可以使用以下代碼。
var application = function(){ //私有變量和函數 var components = new Array(); //初始化 components.push(new BaseComponent()); //創建application 的一個局部副本 var app = new BaseComponent(); //公共接口 app.getComponentCount = function(){ return components.length; }; app.registerComponent = function(component){ if (typeof component == "object"){ components.push(component); } }; //返回這個副本 return app; }();
在這個重寫後的應用程序(application)單例中,首先也是像前面例子中一樣定義了私有變量。主要的不同之處在於命名變量app 的創建過程,因為它必須是BaseComponent 的實例。這個實例實際上是application 對象的局部變量版。此後,我們又為app 對象添加了能夠訪問私有變量的公有方法。最後一步是返回app 對象,結果仍然是將它賦值給全局變量application。
第七章 函數表達式