函式表示式,遞迴,作用域,作用域鏈,閉包,閉包與變數
函式表示式
函式表示式是JavaScript中的一個既強大又容易令人困惑的特性。定義函式的方式有兩種:一種是函式宣告(沒錯,不同於C語言之類的),另一種就是函式表示式。
函式申明(這相當於C語言的函式定義)的語法是這樣的:
function functionName(arg0,arg1,arg2){
//函式體
}
首先是function關鍵字,然後是函式的名字,這就是指定函式名的方式。Firefox、Safari、Chrome和Opera都給函式定義了一個非標準的name屬性,通過這個屬性可以訪問到給函式指定的名字。這個屬性的值永遠等於跟在function關鍵字後面的識別符號。
//只在Firefox、Safari、Chrome和Opera有效
alert(functionName.name); //“functionName”
關於函式宣告,它的一個重要特徵就是函式申明提升,意思是在執行程式碼之前會先讀取函式宣告。這就意味著可以把函式宣告放在呼叫它的語句後面。(神奇~先呼叫後定義?嘿嘿,這就是太沉陷於C語言了哦~)
sayHi();
function sayHi(){
alert("Hi~");
}
這個例子不會丟擲錯誤,因為在程式碼執行之前會先讀取函式宣告。
第二種建立函式的方式是使用函式表示式。函式表示式有幾種不同的語法形式。下面是最常見的一種形式。
var functionName=function(arg0,arg1,arg2){
//函式體
};
這種形式看起來好像是常規的變數賦值語句,即建立一個函式並將它賦值給變數functionName。這種情況下建立的函式叫做匿名函式,因為function關鍵字後面沒有識別符號(你給它起的名字)。(匿名函式有時候也叫拉姆達函式。)匿名函式的name屬性是空字串。
函式表示式與其他表示式一樣,在使用前必須先賦值。以下程式碼會導致錯誤。
sayHi();
var sayHi=function(){
alert("Hi~");
};
理解函式提升的關鍵,就是理解函式申明與函式表示式之間的區別。例如,執行以下程式碼的結果可能會讓人意想不到。
//不要這麼做!
if(condition){
function sayHi(){
alert("Hi!");
}
} else {
function sayHi(){
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.
能夠建立函式再賦值給變數,也就能夠把函式作為其他函式的值返回。
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;
}
};
}
createComparisonFunction()就返回了一個匿名函式。返回的函式可能會被賦值給一個變數,或者以其他方式被呼叫;不過,在createComparisonFunction()函式內部,它是匿名的。在把函式當成值來使用的情況下,都可以使用匿名函式。不過,這並不是匿名函式的唯一用途。
遞迴
遞迴函式是在一個函式通過名字呼叫自身的情況下構成的,如下所示:
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);
}
});
以上程式碼建立了一個名為f()的命名函式表示式,然後將它賦值給變數factorial。即便把函式賦值給了另一個變數,函式的名字f仍然有效,所以遞迴呼叫照樣能正確完成。這種方式在嚴格模式和非嚴格模式下都行得通。
JavaScript中,JavaScript裡一切都是物件,包括函式。函式物件和其它物件一樣,擁有可以通過程式碼訪問的屬性和一系列僅供JavaScript引擎訪問的內部屬性。其中一個內部屬性是作用域,包含了函式被建立的作用域中物件的集合,稱為函式的作用域鏈。
作用域(scope)
通常來說一段程式程式碼中使用的變數和函式並不總是可用的,限定其可用性的範圍即作用域,作用域的使用提高了程式邏輯的區域性性,增強程式的可靠性,減少名字衝突。
作用域鏈(scope chain)
作用域鏈決定了哪些資料能被函式訪問。當一個函式建立後,它的作用域鏈會被建立此函式的作用域中可訪問的資料物件填充。
閉包
有不少開發人員總是搞不清匿名函式和閉包這兩個概念,因此經常混用。閉包是指有權訪問另一個函式作用域中的變數的函式。在javascript中,只有函式內部的子函式才能讀取區域性變數,所以閉包可以理解成“定義在一個函式內部的函式“。在本質上,閉包是將函式內部和函式外部連線起來的橋樑。建立閉包的常見方式,就是在一個函式內部建立另一個函式。
舉個例子:
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()的作用域。要徹底搞清楚其中的細節,必須從理解函式被呼叫的時候都會發生什麼入手。
當某個函式被呼叫時,會建立一個執行環境及相應的作用域鏈。然後,使用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()函式,然後又在全域性作用域中呼叫了它。當呼叫compare()時,會建立一個包含arguments、value1和value2的活動物件。全域性執行環境的變數物件(包含result和compare)在compare()執行環境的作用域鏈中則處於第二位。
上述例子中compare()函式執行時的作用域鏈:
後臺的每個執行環境都有一個表示變數的物件——變數物件。全域性環境的變數物件始終存在,而像compare()函式這樣的區域性環境的變數物件,則只在函式執行的過程中存在。在建立compare()函式時,會建立一個預先包含全域性變數物件的作用域鏈,這個作用域鏈被儲存在內部的[[Scope]](中文意思:作用域)屬性中。當呼叫compare()函式時,會為函式建立一個執行環境,然後通過複製函式的[[Scope]]屬性中的物件構建起執行環境的作用域鏈。此後,又有一個活動物件(在此作為變數物件使用)被建立並被推入執行環境作用域鏈的前端。對於這個例子中compare()函式執行環境而言,其作用域鏈中包含兩個變數物件:本地活動物件和全域性變數物件。顯然,作用域鏈本質上是一個指向變數物件的指標列表,它只引用但不實際包含變數物件。
無論什麼時候在函式中訪問一個變數時,就會從作用域鏈中搜索具有相應名字的變數。一般來講,當函式執行完畢後,區域性活動物件就會被銷燬,記憶體中僅儲存全域性作用域(全域性執行環境的變數物件)。但是,閉包的情況又有所不同。
在另一個函式內部定義的函式會將包含函式(即外部函式)的活動物件新增到它的作用域鏈中。因此,在createComparisonFunction()函式內部定義的匿名函式的作用域鏈中,實際上將會包含外部函式createComparisonFunction()的活動物件。
在匿名函式從createComparisonFunction()中被返回後,它的作用域鏈被初始化為包含createComparisonFunction()函式的活動物件和全域性變數物件。這樣,匿名函式就可以訪問在createComparisonFunction()中定義的所有變數。更為重要的是,createComparisonFunction()函式在執行完畢後,其活動物件也不會被銷燬,因為匿名函式的作用域鏈仍然在引用這個活動物件。換句話說,當createComparisonFunction()函式返回後,其執行環境的作用域鏈會被銷燬,但它的活動物件仍然會留在記憶體中;直到匿名函式被銷燬後,createComparisonFunction()的活動物件才會被銷燬,例如:
//建立函式
var compareNames=createComparisonFunction("name"); // compareNames是匿名函式
//呼叫函式
var result=compareNames({name:"Nicholas"},{name:"Greg"});
//解除對匿名函式的引用(以便釋放記憶體)
compareNames=null;
首先,建立的比較函式被儲存在變數compareNames中。而通過將compareNames設定為等於null解除該函式的引用,就等於通知垃圾回收例程將其清除。隨著匿名函式的作用域鏈被銷燬,其他作用域(除了全域性作用域)也都可以安全地銷燬了。
呼叫compareNames()的過程中產生的作用域鏈之間的關係如下:
閉包與變數
作用域鏈的這種配置機制引出了一個值得注意的副作用,即閉包只能取得包含函式中任何變數的最後一個值。別忘了閉包所儲存的是整個變數物件,而不是某個特殊的變數。
舉個例子:
function createFunctions(){
var result=new Array();
for(var i=0;i<10;i++){
result[i]=function(){
return i;
};
}
return result;
}
這個函式會返回一個函式陣列。表面上看,似乎每個函式都應該返回自己的索引值,即位置0的函式返回0,位置1的函式返回1,以此類推。但實際上,每個函式都返回0。因為每個函式的作用域鏈中都儲存著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變數的一個副本,因此就可以返回各自不同的數值了。