定義函數的方式有兩種:一種是函數聲明,另一種是函數表達式。函數聲明的語法是這樣的。
function functionName(arg0, arg1, arg2){
//函數體
}
說明:Firefox、Safari、Chrome
和Opera
都給函數定義了一個非標準的name
屬性,通過這個屬性可以訪問到給定函數指定的名字。
console.log(functionName.name);//"functionName"
關于函數聲明,它的一個重要特征就是函數聲明提升,意思是在執行代碼之前會先讀取函數聲明。這意味著可以版函數聲明放在調用它的語句后面。
sayHi();
function sayHi(){
console.log("Hi");
}
第二種創建函數的方式是使用函數表達式。函數表達式有幾種不同的語法形式,下面是最常見的一種。
var functionName = function(arg0, arg1, arg2){
//函數體
};
說明:這種情況下創建的函數叫做匿名函數,因為function
關鍵字后面沒有標識符。匿名函數的name
屬性是空字符串。函數表達式與其他表達式一樣,在使用前必須先賦值。
理解函數提升的關鍵,就是理解函數聲明與函數表達式之間的區別,如:
var condition = true;
if(condition){
function sayHi(){
alert("Hi!");
}
} else {
function sayHi(){
alert("Yo!");
}
}
sayHi();
說明:表面上看以上代碼表示在condition
為true
時,使用一個sayHi()
的定義;否則使用另一個定義。實際上,這是一個無效語法,JS
引擎會嘗試修正錯誤,將其轉換為合理的狀態。但是各瀏覽器修正錯誤的做法并不一致,大多數瀏覽器返回第二個聲明,忽略condition
。因為這種使用方式很危險,不應該出現。不過,如果使用函數表達式就沒有什么問題了。
var condition = true;
var sayHi;
//never do this!
if(condition){
sayHi = function(){
alert("Hi!");
};
} else {
sayHi = function(){
alert("Yo!");
};
}
sayHi();
一、遞歸
遞歸函數是在一個函數通過名字調用自身的情況下構成,如下所示:
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()
置為空,當調用anotherFactorial()
時,其內部需要調用factorial()
,而此時卻為null
,于是出錯。這種情況下,使用arguments.callee
可以解決問題。
function factorial(num){
if (num <= 1){
return 1;
} else {
return num * arguments.callee(num-1);
}
}
說明:arguments.callee
是一個指向正在執行的函數的指針,因此可以用它來實現對函數的遞歸調用。但是在嚴格模式下,不能這樣使用,可以使用命名函數表達式來達到相同的結果:
var factorial = (function f(num){
if(num <= 1){
return 1;
}else{
return num * f(num - 1);
}
});
以上代碼創建了一個名為f
的命名函數,然后將其賦值給factorical
,即便把函數賦值給了另一個變量,函數名f
仍然有效。
二、閉包
閉包是指有權訪問另一個函數作用域中的變量的函數。有關如何創建作用域鏈以及作用域鏈有什么用處的細節,對徹底理解閉包至關重要。當某個函數被調用時,會創建一個執行環境及相應的作用域鏈。然后,使用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
)在compa()
執行環境的作用域鏈中則處于第二位。如圖所示:
說明:
其實很好理解,就是在調用
compare()
函數時,會創建一個作用域鏈,這個作用域鏈像一個指針數組,首個元素都是指向其本身的活動對象(就是其本身函數體中定義的一些變量),其次就是包含函數(即本函數的上一層或叫包含本函數的函數或環境,此處沒有,則上一層就是全局變量對象)的活動對象,此處為全局變量對象。在創建
compare()
函數時,會創建一個預先包含全局變量對象的作用域鏈,這個作用域鏈被保存在內部的[[Scope]]
屬性中。當調用compare()
函數時,會為函數創建一個執行環境,然后通過復制函數的[[Scope]]
屬性中的對象構建起執行環境中的作用域鏈。此后,又有一個活動對象(在此作為變量對象使用)被創建并被推入執行環境作用域鏈的前端。對于這個例子中compare()
函數的執行環境而言,其作用域鏈中包含兩個變量對象:本地活動對象和全局變量對象。顯然,作用域鏈本質上是一個指向變量對象的指針列表,它只引用但不實際包含變量對象。無論什么時候在函數中訪問一個變量,就會從作用域鏈中搜索具有相應名字的變量。一般來講,當函數執行完畢之后,局部活動對象就會被銷毀,內存中僅保存全局作用域。但是閉包的情況又有所不同。
在另一個函數內部定義的函數會將包含函數(即外部函數)的活動對象添加到它的作用域鏈中。如:
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()
的活動對象,如圖所示:
代碼如下所示:
var compare = createComparisonFunction("name");
var result = compare({name: "Nicholas"}, {name: "Greg"});
說明:在匿名函數從createComparisonFunction()
中被返回后,它的作用域鏈被初始化為包含createComparisonFunction()
函數的活動對象和全局變量(從匿名函數的作用域鏈的后面兩個位置可以看到)。這樣,匿名函數就可以訪問在createComparisonFunction()
函數中定義的所有變量。更為重要的是,createComparisonFunction()
函數在執行完畢后,其活動對象也不會被銷毀,因為匿名函數的作用域鏈仍然在引用這個活動對象,而直到匿名函數被銷毀后,createComparisonFunction()
的活動對象才會被銷毀。如:
var compareNames = createComparisonFunction("name");
var result = compareNames({name: "Nicholas"}, {name: "Greg"});
compareNames = null;
最后一行是將匿名函數銷毀。
2.1 閉包與變量
作用域鏈的這種配置機制引出了一個值得注意的副作用,即閉包只能取得包含函數中任何變量的最后一個值。下面這個例子可以清晰地說明這個問題:
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
,于是每個函數都返回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;
}
//換一種寫法
function createFunctions(){
var result = new Array();
function indexFunc(num){
return function(){
return num;
}
}
for (var i=0; i < 10; i++){
result[i] = indexFunc(i);
}
return result;
}
說明:以上代碼中,沒有直接把閉包賦值給數組,而是定義了一個匿名函數,并將立即執行該匿名函數的結果賦給數組。在調用每個匿名函數時,我們傳入了變量i
,由于函數參數是按值傳遞的,所以就會將變量i的當前值復制給參數num
,而在這個匿名函數內部,又創建并返回了一個訪問num
的閉包,這樣,result
數組中的每個函數都有自己的num
變量副本,因此就可以返回各自的索引了。
2.2 關于this變量
我們知道,this
對象是在運行時基于函數的執行環境綁定的:在全局函數中,this
等于window
,而當函數被作為某個對象的方法調用時,this
等于那個對象。不過,匿名函數的執行環境具有全局性,因此this
對象通常指向window
(當然,在通過call()
或applay()
改變函數執行環境的情況下,this
就會指向其他對象)。但有時候由于編寫閉包的方式不同,這一點可能不會那么明顯,如下:
var name = "The Window";
var object = {
name : "My Object",
getNameFunc : function(){
return function(){
return this.name;
};
}
};
alert(object.getNameFunc()()); //"The Window"(在非嚴格模式下)
說明:這里的this
是指window
,那為什么不是取得其包含作用域(或外部作用域)的this對象呢?這是因為,每個函數在被調用時都會自動取得兩個特殊的變量:this
和arguments
。內部函數在搜索這兩個變量時,只會搜索到其活動對象為止(就是從全局活動對象中開始一層一層向內部搜索,只要搜索到this
變量,則停止搜索),因此永遠不可能直接訪問外部函數中的這兩個變量。不過,把外部作用域中的this
對象保存在一個閉包能夠訪問到的變量里,就可以讓閉包訪問該對象了,如下:
var name = "The Window";
var object = {
name : "My Object",
getNameFunc : function(){
var that = this;
return function(){
return that.name;
};
}
};
alert(object.getNameFunc()()); //"MyObject"
說明:this
的指向在函數定義的時候是不能確定的,只有在函數執行的時候才能確定,實際上,this
的最終指向的是那個調用它的對象。