定義函數的方式有兩種:函數聲明和函數表達式。
// 函數聲明
function fn(){}
函數聲明的一個重要特征就是函數聲明提升,意思是在執行代碼前會先讀取函數聲明。這就意味著可以把函數聲明放在調用它的語句后面。
第二種創建函數的方式是使用函數表達式。這種情況下創建的函數叫匿名函數。
var fn=function(){}
函數表達式與其他表達式一樣,在使用前必須先賦值。以下代碼會導致錯誤。
sayHi(); //錯誤:函數還不存在
var sayHi = function(){
alert("Hi!");
};
能夠創建函數再賦值給變量,也就能夠把函數作為其他函數的值返回。
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()
就返回了一個匿名函數。返回的函數可能會被賦值給一個變量,或者以其他方式被調用;不過,在demo()
函數內部,它是匿名的。在把函數當成值來使用的情況下,都可以使用匿名函數。不過,這并不是匿名函數唯一的用途。
遞歸
遞歸函數是在一個函數內通過名字調用自身的情況下構成的。
function factorial(num) {
if(num<=1){
return 1;
}else{
return num*factorial(num-1);
}
}
下面的代碼卻可能導致它出錯。
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);
}
});
閉包
閉包是指有權訪問另一個函數作用域中的變量的函數。創建閉包的常見方式就是在一個函數內部創建另一個函數。
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;
}
};
}
當某個函數被調用時,會創建一個執行環境及相應的作用域鏈。然后,使用arguments
和其他命名參數的值來初始化函數的活動對象。但在作用域鏈中,外部函數的活動對象始終處于第二位,外部函數的外部函數的活動對象處于第三位,.....直至作為作用域鏈終點的全局執行環境。
在函數執行過程中,為讀取和寫入變量的值,就需要在作用域鏈中查找變量。
function compare(value1, value2){
if (value1 < value2){
return -1;
} else if (value1 > value2){
return 1;
} else {
return 0;
}
}
var result = compare(5, 10);
當調用compare()
時,會創建一個包含arguments
、value1
和value2
的活動對象。全局執行環境的變量對象(包含result
和compare
)在compare()
執行環境的作用域鏈中則處于第二位。
后臺的每個執行環境都有一個表示變量的對象——變量對象。全局環境的變量對象始終存在,而像
compare()
函數這樣的局部環境的變量對象,則只在函數執行的過程中存在。在創建compare()
函數時,會創建一個預先包含全局變量對象的作用域鏈,這個作用域鏈被保存在內部的[[Scope]]
屬性中。 當調用compare()
函數時,會為函數創建一個執行環境,然后通過復制函數的[[Scope]]
屬性中的對象構建起執行環境的作用域鏈。此后,又有一個活動對象(在此作為變量對象使用)被創建并被推入執行環境作用域鏈的前端。對于這個例子中compare()
函數的執行環境而言,其作用域鏈中包含兩個變量對象:本地活動對象和全局變量對象。顯然,作用域鏈本質上是一個指向變量對象的指針列表,它只引用但不實際包含變量對象。無論什么時候在函數中訪問一個變量時,就會從作用域鏈中搜索具有相應名字的變量。一般來講, 當函數執行完畢后,局部活動對象就會被銷毀,內存中僅保存全局作用域(全局執行環境的變量對象)。但是,閉包的情況又有所不同。
在另一個函數內部定義的函數會將包含函數(即外部函數)的活動對象添加到它的作用域鏈中。因此,在
demo()
函數內部定義的匿名函數的作用域鏈中,實際上將會包含外部函數 demo()
的活動對象。
var compare = demo("name");
var result = compare({ name: "Nicholas" }, { name: "Greg" });
在匿名函數從demo()
中被返回后,它的作用域鏈被初始化為包含demo()
函數的活動對象和全局變量對象。這樣,匿名函數就可以訪問在demo()
中定義的所有變量。更為重要的是,demo()
函數在執行完畢后,其活動對象也不會被銷毀,因為匿名函數的作用域鏈仍然在引用這個活動對象。換句話說,當demo()
函數返回后,其執行環境的作用域鏈會被銷毀,但它的活 動對象仍然會留在內存中;直到匿名函數被銷毀后,demo()
的活動對象才會被銷毀。
//創建函數
var compareNames = demo("name");
//調用函數
var result = compareNames({ name: "Nicholas" }, { name: "Greg" });
//解除對匿名函數的引用(以便釋放內存)
compareNames = null;
首先,創建的比較函數被保存在變量compareNames
中。而通過將compareNames
設置為等于null
解除該函數的引用,就等于通知垃圾回收例程將其清除。隨著匿名函數的作用域鏈被銷毀,其他作用域 (除了全局作用域)也都可以安全地銷毀了。
由于閉包會攜帶包含它的函數的作用域,因此會比其他函數占用更多的內存。過度使用閉包可能會導致內存占用過多。
閉包與變量
閉包只能取得包含函數中任何變量的最后一個值。閉包所保存的是整個變量對象,而不是某個特殊的變量。
function createFunctions(){
var result = new Array();
for (var i=0; i < 10; i++){
result[i] = function(){
return i;
};
}
return result;
}
這個函數會返回一個函數數組。表面上看,似乎每個函數都應該返自己的索引值,即位置0的函數返回0,位置1的函數返回1,以此類推。但實際上,每個函數都返回10。因為每個函數的作用域鏈中都保存createFunctions()
函數的活動對象 , 所以它們引用的都是同一個變量i
。 當createFunctions()
函數返回后,變量i
的值是10,此時每個函數都引用著保存變量i
的同一個變量 對象,所以在每個函數內部i
的值都是10。但是,我們可以通過創建另一個匿名函數強制讓閉包的行為符合預期。
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()
函數后,每個函數就會返回各自不同的索引值了。在這個版本中,我們沒有直接把閉包賦值給數組,而是定義了一個匿名函數,并將立即執行該匿名函數的結果賦給數組。這里的匿名函數有一個參數num
,也就是最終的函數要返回的值。在調用每個匿名函數時,我們傳入了變量i
。由于函數參數是按值傳遞的,所以就會將變量 i 的當前值復制給參數num
。而在這個匿名函數內部,又創建并返回了一個訪問num
的閉包。這樣一來,result
數組中的每個函數都有自己num
變量的一個副本,因此就可以返回各自不同的數值了。
關于this對象
在閉包中使用this
對象也可能會導致一些問題。我們知道,this
對象是在運行時基于函數的執 行環境綁定的:在全局函數中,this
等于window
,而當函數被作為某個對象的方法調用時,this
等于那個對象。不過,匿名函數的執行環境具有全局性,因此其this
對象通常指向window
。但有時候由于編寫閉包的方式不同,這一點可能不會那么明顯。
var name = "The Window";
var object = {
name : "My Object",
getNameFunc : function(){
return function(){
return this.name;
};
}
};
alert(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;
};
}
};
alert(object.getNameFunc()()); //"My Object"
this
和arguments
也存在同樣的問題。如果想訪問作用域中的arguments
對象,必須將對該對象的引用保存到另一個閉包能夠訪問的變量中。
在幾種特殊情況下,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",在非嚴格模式下
內存泄漏
由于IE9之前的版本對JScript
對象和COM
對象使用不同的垃圾收集例程,因此閉包在IE的這些版本中會導致一些特殊的問題。具體來說,如果閉包的作用域鏈中保存著一個HTML元素,那么就意味著該元素將無法被銷毀。
function assignHandler(){
var element = document.getElementById("someElement");
element.onclick = function(){
alert(element.id);
};
}
以上代碼創建了一個作為element
元素事件處理程序的閉包,而這個閉包則又創建了一個循環引用。由于匿名函數保存了一個對assignHandler()
的活動對象的引用,因此就會導致無法減少element
的引用數。只要匿名函數存在,element
的引用數至少也是1,因此它所占用的內存就永遠不會被回收。不過,這個問題可以通過稍微改寫一下代碼來解決。
function assignHandler(){
var element = document.getElementById("someElement");
var id = element.id;
element.onclick = function(){
alert(id);
};
element = null;
}
在上面的代碼中,通過把element.id
的一個副本保存在一個變量中,并且在閉包中引用該變量消除了循環引用。但僅僅做到這一步,還是不能解決內存泄漏的問題。必須要記住:閉包會引用包含函數的整個活動對象,而其中包含著element
。即使閉包不直接引用element
,包含函數的活動對象中也仍然會保存一個引用。因此,有必要把element
變量設置為null
。這樣就能夠解除對DOM對象的引用,順利地減少其引用數,確保正常回收其占用的內存。
模仿塊級作用域
JS沒有塊級作用域的概念。這意味著在塊語句中定義的變量,實際上是在包含函數中而非語句中創建的。
functìon outputNumbers(count){
for (var i=0;i<count;i++){
alert(i) ;
}
alert(i); //計數
}
在JS中,變量i
是定義在ouputNumbers()
的活動對象中的,因此從它有定義開始,就可以在函數內部隨處訪問它。即使像下面這樣錯誤地重新聲明同一個變量,也不會改變它的值。
function outputNumbers(count){
for (var i =0;i<count;i++) {
alert(i);
}
var i; //重新聲明變量
alert(i) ; // 計數
}
JS從來不會告訴你是否多次聲明了同一個變量;遇到這種情況,它只會對后續的聲明視而不見(不過,它會執行后續聲明中的變量初始化)。匿名函數可以用來模仿塊級作用域并避免這個問題。
用作塊級作用域(通常稱為私有作用域)的匿名函數的語法如下所示。
(function(){
//這里是塊級作用域
})();
以上代碼定義并立即調用了一個匿名函數。將函數聲明包含在一對圓括號中,表示它實際上是一個函數表達式。而緊隨其后的另一對圓括號會立即調用這個函數。
無論在什么地方,只要臨時需要一些變量,就可以使用私有作用域,例如:
function outputNumbers(count){
(function () {
for (var i=0; i < count; i++){
alert(i);
}
})();
alert(i); //導致一個錯誤!
}
這種做法可以減少閉包占用的內存問題,因為沒有指向匿名函數的引用。只要名函數執行完畢,就可以立即銷段其作用域鏈了。
私有變量
嚴格來講,JS中沒有私有成員的概念;所有對象屬性都是公有的。不過,倒是有一個私有變量的概念。任何在函數中定義的變量,都可以認為是私有變量,因為不能在函數的外部訪問這些變量。私有變量包括函數的參數、局部變量和在函數內部定義的其他函數。
function add (num1 , num2 ) {
var sum = num1 + num2;
return sum;
}
在這個函數內部有3個私有變量:num1
,num2
和sum
。在函數內部可以訪問這幾個變量,但在函數外部則不能訪問它們。如果在這個函數內部創建一個閉包,那么閉包通過自己的作用域鏈也可以訪問這些變量。而利用這一點,就可以創建用于訪問私有變量的公有方法。
我們把有權訪問私有變量和私有函數的公有方法稱為特權方法。有兩種在對象上創建特權方法的方式。第一種是在構造函數中定義特權方法。
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("Nicholas");
alert(person.getName()); //"Nicholas"
person.setName("Greg");
alert(person.getName()); //"Greg"
在構造函數中定義特權方法也有一個缺點,那就是你必須使用構造函數模式來達到這個目的。構造函數模式的缺點是針對每個實例都會創建同樣一組新方法,而使用靜態私有變量來實現特權方法就可以避免這個問題。
靜態私有變量
通過在私有作用域中定義私有變量或函數,同樣也可以創建特權方法。
(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("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"
這個例子中的Person
構造函數與getName()
和setName()
方法一樣,都有權訪問私有變量name
。
在這種模式下,變量name
就變成了一個靜態的、由所有實例共享的屬性。也就是說,在一個實例上調用setName()
會影響所有實例。而調用setName()
或新建一個Person
實例都會賦予name
屬性一個新值。結果就是所有實例都會返回相同的值。
以這種方式創建靜態私有變量會因為使用原型而增進代碼復用,但每個實例都沒有自己的私有變量。到底是使用實例變量,還是靜態私有變量,最終還是要視你的具體需求而定。
多查找作用域鏈中的一個層次,就會在一定程度上影響查找速度。而這正是使用閉包和私有變量的一個顯明的不足之處。
模塊模式
模塊模式是為單例創建私有變量和特權方法。所謂單例,指的就是只有一個實例的對象。 按照慣例,JS是以對象字面量的方式來創建單例對象的。
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
操作符來檢查其對象類型了。
增強的模塊模式
在返回對象之前加入對其增強的代碼。這種增強的模塊模式適合那些單例必須是某種類型的實例,同時還必須添加某些屬性和方法對其加以增強的情況。
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
。
小結
在JS編程中,函數表達式是一種非常有用的技術。使用函數表達式可以無須對函數命名, 從而實現動態編程。匿名函數,也稱為拉姆達函數,是一種使用JS函數的強大方式。以下總結了函數表達式的特點。
- 函數表達式不同于函數聲明。函數聲明要求有名字,但函數表達式不需要。沒有名字的函數表達式也叫做匿名函數。
- 在無法確定如何引用函數的情況下,遞歸函數就會變得比較復雜;
- 遞歸函數應該始終使用
arguments.callee
來遞歸地調用自身,不要使用函數名,函數名可能會發生變化。
當在函數內部定義了其他函數時,就創建了閉包。閉包有權訪問包含函數內部的所有變量。
- 在后臺執行環境中,閉包的作用域鏈包含著它自己的作用域、包含函數的作用域和全局作用域。
- 通常,函數的作用域及其所有變量都會在函數執行結束后被銷毀。
- 但是,當函數返回了一個閉包時,這個函數的作用域將會一直在內存中保存到閉包不存在為止。
使用閉包可以在JS中模仿塊級作用域(JS本身沒有塊級作用域的概念)。
- 創建并立即調用一個函數,這樣既可以執行其中的代碼,又不會在內存中留下對該函數的引用。
- 結果就是函數內部的所有變量都會被立即銷毀——除非將某些變量賦值給了包含作用域(即外部作用域)中的變量。
閉包還可以用于在對象中創建私有變量。
- 即使JS中沒有正式的私有對象屬性的概念,但可以使用閉包來實現公有方法,而通過公有方法可以訪問在包含作用域中定義的變量。
- 有權訪問私有變量的公有方法叫做特權方法。
- 可以使用構造函數模式、原型模式來實現自定義類型的特權方法,也可以使用模塊模式、增強的模塊模式來實現單例的特權方法。
JS中的函數表達式和閉包都是極其有用的特性,利用它們可以實現很多功能。不過,因為創建閉包必須維護額外的作用域,所以過度使用它們可能會占用大量內存。