定義函數(shù)的方式有兩種:一種是函數(shù)聲明榨惰,另一種是函數(shù)表達(dá)式。函數(shù)聲明的語法是這樣的静汤。
function functionName(arg0, arg1, arg2){
//函數(shù)體
}
說明:Firefox琅催、Safari居凶、Chrome
和Opera
都給函數(shù)定義了一個(gè)非標(biāo)準(zhǔn)的name
屬性,通過這個(gè)屬性可以訪問到給定函數(shù)指定的名字藤抡。
console.log(functionName.name);//"functionName"
關(guān)于函數(shù)聲明侠碧,它的一個(gè)重要特征就是函數(shù)聲明提升,意思是在執(zhí)行代碼之前會先讀取函數(shù)聲明缠黍。這意味著可以版函數(shù)聲明放在調(diào)用它的語句后面弄兜。
sayHi();
function sayHi(){
console.log("Hi");
}
第二種創(chuàng)建函數(shù)的方式是使用函數(shù)表達(dá)式。函數(shù)表達(dá)式有幾種不同的語法形式瓷式,下面是最常見的一種替饿。
var functionName = function(arg0, arg1, arg2){
//函數(shù)體
};
說明:這種情況下創(chuàng)建的函數(shù)叫做匿名函數(shù),因?yàn)?code>function關(guān)鍵字后面沒有標(biāo)識符贸典。匿名函數(shù)的name
屬性是空字符串视卢。函數(shù)表達(dá)式與其他表達(dá)式一樣,在使用前必須先賦值廊驼。
理解函數(shù)提升的關(guān)鍵据过,就是理解函數(shù)聲明與函數(shù)表達(dá)式之間的區(qū)別,如:
var condition = true;
if(condition){
function sayHi(){
alert("Hi!");
}
} else {
function sayHi(){
alert("Yo!");
}
}
sayHi();
說明:表面上看以上代碼表示在condition
為true
時(shí)妒挎,使用一個(gè)sayHi()
的定義喧伞;否則使用另一個(gè)定義汉嗽。實(shí)際上悼沿,這是一個(gè)無效語法丧叽,JS
引擎會嘗試修正錯(cuò)誤,將其轉(zhuǎn)換為合理的狀態(tài)庸队。但是各瀏覽器修正錯(cuò)誤的做法并不一致积蜻,大多數(shù)瀏覽器返回第二個(gè)聲明,忽略condition
彻消。因?yàn)檫@種使用方式很危險(xiǎn)竿拆,不應(yīng)該出現(xiàn)。不過宾尚,如果使用函數(shù)表達(dá)式就沒有什么問題了丙笋。
var condition = true;
var sayHi;
//never do this!
if(condition){
sayHi = function(){
alert("Hi!");
};
} else {
sayHi = function(){
alert("Yo!");
};
}
sayHi();
一、遞歸
遞歸函數(shù)是在一個(gè)函數(shù)通過名字調(diào)用自身的情況下構(gòu)成煌贴,如下所示:
function factorial(num){
if (num <= 1){
return 1;
} else {
return num * factorial(num-1);
}
}
這是一個(gè)經(jīng)典的遞歸階乘函數(shù)御板。雖然表面上看沒什么問題,但是下面的代碼卻可能導(dǎo)致其出錯(cuò)牛郑。
var anotherFactorial = factorial;
factorial = null;
alert(anotherFactorial(4)); //出錯(cuò)!
說明:雖然我們先將factorial()
函數(shù)賦給了anotherFactorial
怠肋,但是此后將factorial()
置為空,當(dāng)調(diào)用anotherFactorial()
時(shí)淹朋,其內(nèi)部需要調(diào)用factorial()
笙各,而此時(shí)卻為null
钉答,于是出錯(cuò)。這種情況下杈抢,使用arguments.callee
可以解決問題数尿。
function factorial(num){
if (num <= 1){
return 1;
} else {
return num * arguments.callee(num-1);
}
}
說明:arguments.callee
是一個(gè)指向正在執(zhí)行的函數(shù)的指針,因此可以用它來實(shí)現(xiàn)對函數(shù)的遞歸調(diào)用惶楼。但是在嚴(yán)格模式下右蹦,不能這樣使用,可以使用命名函數(shù)表達(dá)式來達(dá)到相同的結(jié)果:
var factorial = (function f(num){
if(num <= 1){
return 1;
}else{
return num * f(num - 1);
}
});
以上代碼創(chuàng)建了一個(gè)名為f
的命名函數(shù)歼捐,然后將其賦值給factorical
何陆,即便把函數(shù)賦值給了另一個(gè)變量,函數(shù)名f
仍然有效窥岩。
二甲献、閉包
閉包是指有權(quán)訪問另一個(gè)函數(shù)作用域中的變量的函數(shù)宰缤。有關(guān)如何創(chuàng)建作用域鏈以及作用域鏈有什么用處的細(xì)節(jié)颂翼,對徹底理解閉包至關(guān)重要。當(dāng)某個(gè)函數(shù)被調(diào)用時(shí)慨灭,會創(chuàng)建一個(gè)執(zhí)行環(huán)境及相應(yīng)的作用域鏈朦乏。然后,使用arguments
和其他命名參數(shù)的值來初始化函數(shù)的活動對象(就是函數(shù)體中定義的變量)氧骤。但在作用域鏈中呻疹,外部函數(shù)的活動對象始終處于第二位,外部函數(shù)的外部函數(shù)的活動對象處于第三位筹陵,一直到作用域鏈終點(diǎn)的全局執(zhí)行環(huán)境刽锤。舉例說明:
function compare(value1, value2){
if(value1 < value2){
return -1;
}else if(value1 > value2){
return 1;
}else{
return 0;
}
}
var result = compare(5, 10);
當(dāng)調(diào)用compare()
時(shí),會創(chuàng)建一個(gè)包含arguments朦佩、value1
和value2
的活動對象并思。全局執(zhí)行環(huán)境對象(包含result
和compare
)在compa()
執(zhí)行環(huán)境的作用域鏈中則處于第二位。如圖所示:
說明:
其實(shí)很好理解语稠,就是在調(diào)用
compare()
函數(shù)時(shí)宋彼,會創(chuàng)建一個(gè)作用域鏈,這個(gè)作用域鏈像一個(gè)指針數(shù)組仙畦,首個(gè)元素都是指向其本身的活動對象(就是其本身函數(shù)體中定義的一些變量)输涕,其次就是包含函數(shù)(即本函數(shù)的上一層或叫包含本函數(shù)的函數(shù)或環(huán)境,此處沒有慨畸,則上一層就是全局變量對象)的活動對象莱坎,此處為全局變量對象。在創(chuàng)建
compare()
函數(shù)時(shí)寸士,會創(chuàng)建一個(gè)預(yù)先包含全局變量對象的作用域鏈檐什,這個(gè)作用域鏈被保存在內(nèi)部的[[Scope]]
屬性中瞳收。當(dāng)調(diào)用compare()
函數(shù)時(shí),會為函數(shù)創(chuàng)建一個(gè)執(zhí)行環(huán)境厢汹,然后通過復(fù)制函數(shù)的[[Scope]]
屬性中的對象構(gòu)建起執(zhí)行環(huán)境中的作用域鏈螟深。此后,又有一個(gè)活動對象(在此作為變量對象使用)被創(chuàng)建并被推入執(zhí)行環(huán)境作用域鏈的前端烫葬。對于這個(gè)例子中compare()
函數(shù)的執(zhí)行環(huán)境而言界弧,其作用域鏈中包含兩個(gè)變量對象:本地活動對象和全局變量對象。顯然搭综,作用域鏈本質(zhì)上是一個(gè)指向變量對象的指針列表垢箕,它只引用但不實(shí)際包含變量對象。無論什么時(shí)候在函數(shù)中訪問一個(gè)變量兑巾,就會從作用域鏈中搜索具有相應(yīng)名字的變量条获。一般來講,當(dāng)函數(shù)執(zhí)行完畢之后蒋歌,局部活動對象就會被銷毀帅掘,內(nèi)存中僅保存全局作用域。但是閉包的情況又有所不同堂油。
在另一個(gè)函數(shù)內(nèi)部定義的函數(shù)會將包含函數(shù)(即外部函數(shù))的活動對象添加到它的作用域鏈中修档。如:
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()
函數(shù)內(nèi)部定義的匿名函數(shù)的作用域鏈中府框,實(shí)際上會包含外部函數(shù)createComparisonFunction()
的活動對象吱窝,如圖所示:
代碼如下所示:
var compare = createComparisonFunction("name");
var result = compare({name: "Nicholas"}, {name: "Greg"});
說明:在匿名函數(shù)從createComparisonFunction()
中被返回后,它的作用域鏈被初始化為包含createComparisonFunction()
函數(shù)的活動對象和全局變量(從匿名函數(shù)的作用域鏈的后面兩個(gè)位置可以看到)迫靖。這樣院峡,匿名函數(shù)就可以訪問在createComparisonFunction()
函數(shù)中定義的所有變量。更為重要的是系宜,createComparisonFunction()
函數(shù)在執(zhí)行完畢后照激,其活動對象也不會被銷毀,因?yàn)槟涿瘮?shù)的作用域鏈仍然在引用這個(gè)活動對象蜈首,而直到匿名函數(shù)被銷毀后实抡,createComparisonFunction()
的活動對象才會被銷毀。如:
var compareNames = createComparisonFunction("name");
var result = compareNames({name: "Nicholas"}, {name: "Greg"});
compareNames = null;
最后一行是將匿名函數(shù)銷毀欢策。
2.1 閉包與變量
作用域鏈的這種配置機(jī)制引出了一個(gè)值得注意的副作用吆寨,即閉包只能取得包含函數(shù)中任何變量的最后一個(gè)值。下面這個(gè)例子可以清晰地說明這個(gè)問題:
function createFunctions(){
var result = new Array();
for (var i=0; i < 10; i++){
result[i] = function(){
return i;
};
}
return result;
}
說明:這個(gè)函數(shù)會返回一個(gè)函數(shù)數(shù)組踩寇。表面上看啄清,似乎每個(gè)函數(shù)都應(yīng)返回自己的索引值,即位置0
的函數(shù)返回0
,位置1
的函數(shù)返回1
辣卒,以此類推掷贾。但是實(shí)際上,每個(gè)函數(shù)都返回10
荣茫。因?yàn)槊總€(gè)函數(shù)的作用域鏈中都保存著createFunctions()
函數(shù)的活動對象想帅,所以它們引用的都是同一個(gè)變量i
。當(dāng)createFunctions()
函數(shù)返回后啡莉,變量i的值是10
港准,于是每個(gè)函數(shù)都返回10
。這里數(shù)組中的每個(gè)匿名函數(shù)就是一個(gè)閉包咧欣∏掣祝可以通過創(chuàng)建另一個(gè)匿名函數(shù)強(qiáng)制讓閉包的行為符合預(yù)期,如下所示:
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;
}
說明:以上代碼中魄咕,沒有直接把閉包賦值給數(shù)組衩椒,而是定義了一個(gè)匿名函數(shù),并將立即執(zhí)行該匿名函數(shù)的結(jié)果賦給數(shù)組哮兰。在調(diào)用每個(gè)匿名函數(shù)時(shí)毛萌,我們傳入了變量i
,由于函數(shù)參數(shù)是按值傳遞的奠蹬,所以就會將變量i的當(dāng)前值復(fù)制給參數(shù)num
朝聋,而在這個(gè)匿名函數(shù)內(nèi)部,又創(chuàng)建并返回了一個(gè)訪問num
的閉包囤躁,這樣,result
數(shù)組中的每個(gè)函數(shù)都有自己的num
變量副本荔睹,因此就可以返回各自的索引了狸演。
2.2 關(guān)于this變量
我們知道,this
對象是在運(yùn)行時(shí)基于函數(shù)的執(zhí)行環(huán)境綁定的:在全局函數(shù)中僻他,this
等于window
宵距,而當(dāng)函數(shù)被作為某個(gè)對象的方法調(diào)用時(shí),this
等于那個(gè)對象吨拗。不過满哪,匿名函數(shù)的執(zhí)行環(huán)境具有全局性,因此this
對象通常指向window
(當(dāng)然劝篷,在通過call()
或applay()
改變函數(shù)執(zhí)行環(huán)境的情況下哨鸭,this
就會指向其他對象)。但有時(shí)候由于編寫閉包的方式不同娇妓,這一點(diǎn)可能不會那么明顯像鸡,如下:
var name = "The Window";
var object = {
name : "My Object",
getNameFunc : function(){
return function(){
return this.name;
};
}
};
alert(object.getNameFunc()()); //"The Window"(在非嚴(yán)格模式下)
說明:這里的this
是指window
,那為什么不是取得其包含作用域(或外部作用域)的this對象呢哈恰?這是因?yàn)橹还溃總€(gè)函數(shù)在被調(diào)用時(shí)都會自動取得兩個(gè)特殊的變量:this
和arguments
志群。內(nèi)部函數(shù)在搜索這兩個(gè)變量時(shí),只會搜索到其活動對象為止(就是從全局活動對象中開始一層一層向內(nèi)部搜索蛔钙,只要搜索到this
變量锌云,則停止搜索),因此永遠(yuǎn)不可能直接訪問外部函數(shù)中的這兩個(gè)變量吁脱。不過宾抓,把外部作用域中的this
對象保存在一個(gè)閉包能夠訪問到的變量里,就可以讓閉包訪問該對象了豫喧,如下:
var name = "The Window";
var object = {
name : "My Object",
getNameFunc : function(){
var that = this;
return function(){
return that.name;
};
}
};
alert(object.getNameFunc()()); //"MyObject"
說明:this
的指向在函數(shù)定義的時(shí)候是不能確定的石洗,只有在函數(shù)執(zhí)行的時(shí)候才能確定,實(shí)際上紧显,this
的最終指向的是那個(gè)調(diào)用它的對象讲衫。