我們已經(jīng)在第1章討論過黄鳍,在javascript中寻馏,函數(shù)其實就是對象,使函數(shù)不同意其他對象的決定性特點是函數(shù)存在一個被稱為[[Call]]的內(nèi)部屬性道伟。內(nèi)部屬性無法通過代碼訪問而是定義了代碼執(zhí)行時的行為媒咳。ECMAScript為javascript的對象定義了多種內(nèi)部屬性粹排,這些內(nèi)部屬性都用雙重中括號來標(biāo)注。
?[[Call]]屬性是函數(shù)獨有的涩澡,表明該對象可以被執(zhí)行顽耳。由於僅有函數(shù)擁有該屬性,ECMAScript定義typeof操作符對任何具有[[Call]]屬性的對象返回“funciton”妙同。這在過去曾經(jīng)導(dǎo)致了一些問題射富,因為某些瀏覽器曾經(jīng)在正則表達式中包含了[[ Call]]屬性,導(dǎo)致後者被錯誤鑑定為函數(shù),現(xiàn)在渐溶,多有瀏覽器的行為都一致辉浦,typeof不會將正則表達式鑒別為函數(shù)了弄抬。
2.1聲明還是表達式
函數(shù)具有兩種字面形式茎辐,第一種事函數(shù)聲明,以function關(guān)鍵字開頭掂恕,後面跟著函數(shù)的名字拖陆。函數(shù)的內(nèi)容放在大括號內(nèi),例如下面就是函數(shù)聲明懊亡。
function add(num1,num2){return num1+num2}
第二種形式是函數(shù)表達式依啰,funciton關(guān)鍵字後面不需要加上函數(shù)的名字。這種函數(shù)被稱為匿名函數(shù)店枣,因為函數(shù)對象本身沒有名字速警。取而代之的函數(shù)表達式通常會被一個變量或者引用叹誉,下面就是函數(shù)表達式。
var add =function(num1,num2){
return num1+num2;
};
這段代碼時機上將一個函數(shù)作為值賦給變量add闷旧,除了沒有函數(shù)名並在最後多了一個分號以外长豁,函數(shù)表達式幾乎和函數(shù)聲明完全一樣。函數(shù)表達式賦值通常在最後一個分號忙灼,就如同其他對像的賦值一樣匠襟。
雖然這兩種代碼形式頗為相似,但是他們有一個非常重要的區(qū)別该园,函數(shù)聲明會被提升至上下文(要麼是該函數(shù)被聲明時所在的函數(shù)的範(fàn)圍酸舍,要麼是全局範(fàn)圍)的頂部,這意味著你可以先使用函數(shù)後聲明他們里初。例如:
var result =add(5,5);
function add(num,num2){
return num1+num2;
}
這段代碼看上去似乎會造成錯誤啃勉,但實際上可以工總,那是因為javascript引擎將函數(shù)聲明提升至頂部來執(zhí)行青瀑,就好像他被寫成如下形式:
//how the javascript engine interpers the code
function add(num1,num2){
return num1+num2;
}
var result=add(5,5);
javascript能對函數(shù)聲明進行提升璧亮,這是因為引擎提前知道了函數(shù)的名字。而函數(shù)表達式僅能通過變量引用斥难,因此無法提升枝嘶,所以下面這段代碼會導(dǎo)致錯誤
//error
var result =add(5,5);
var add=function(num1,num2){
return num1+num2;
};
只要你始終在使用函數(shù)之前定義他們,你就可以隨意使用函數(shù)聲明或表達式哑诊。
2.2 函數(shù)就是值
函數(shù)是javascript的一大重點群扶,你可以像使用對象一樣使用函數(shù)。也可以將它們福祉給變量镀裤,在對象中添加它們竞阐,將它們當(dāng)成參數(shù)傳遞給別的函數(shù),或從別的函數(shù)中返回暑劝÷嬗ǎ基本上只要是可以使用其他引用值的地方,你就可以使用函數(shù)担猛。這使得javascript的函數(shù)威力無窮幕垦,考慮下面的例子:
function sayHi(){
console.log("Hi");
}
sayHi();//outputs "hi";
var sayHi2=sayHi;
sayHi2();//outputs "hi";
這段代碼首先要有一個函數(shù)聲明sayHi。然後有一個變量sayHi2被創(chuàng)建並被賦予sayHi的值傅联,sayHi和sayHi2現(xiàn)在指向同一個函數(shù)先改,兩者都可以被執(zhí)行,並具有相同的結(jié)果蒸走。為了更好的理解這點仇奶,讓我來看一下用function構(gòu)造函數(shù)重寫具有相同功能的代碼。
var sayHi=new Function("console.log(\"Hi!\");");
sayHi();
var sayHi2=sayHi;
sayHi2(); //outputs "Hi"
Function 構(gòu)造函數(shù)更加清楚地表明sayHi能夠像其他對象一樣被傳來傳去比驻。只要你記住函數(shù)就是對象该溯,很多行為就變得容易理解了岛抄。
例如。你可以將函數(shù)當(dāng)成參數(shù)傳遞給其他的函數(shù)狈茉。javascript數(shù)組的sort()方法接受耶和華 i 個比較函數(shù)作為可選參數(shù)弦撩,每當(dāng)數(shù)組中兩個值需要進行比較時都會調(diào)用此函數(shù)。如果第一個值小於第二個论皆。比較函數(shù)會返回一個負數(shù)益楼。如果第一個值大於第二個,比較函數(shù)會返回一個正數(shù) 点晴,如果兩個值相等感凤,函數(shù)返回0;
在默認情況下粒督,sort()將數(shù)組中的每個對象轉(zhuǎn)換成字符串然後進行比較陪竿。這意味著,你無法在不指定比較函數(shù)的情況下為數(shù)字的數(shù)組進行精確排序屠橄。
var number=[1,5,8,4,7,10,2,6];
numbers.sort(function(first,second)){
return first-second;
}
console.log(numbers);
numbers.sortI();
console.log(numbers);
在本例族跛,被傳遞給sort()的比較函數(shù)其實是一個函數(shù)表達式。請注意它沒有名字锐墙,僅作為引用被傳遞給另一個函數(shù)(著使得它被稱為匿名函數(shù))礁哄。比較函數(shù)對兩個值進行比較相減法以返回正確的結(jié)果。
作為對比溪北,第二次sort()不使用比較函數(shù)桐绒。結(jié)果和預(yù)期不太一樣,1後面跟著的事10.這是因為默認的比較函數(shù)將所有值都轉(zhuǎn)換成字符串比較之拨。
2.3 參數(shù)
javascript函數(shù)的另一個獨特之處在於你可以給函數(shù)傳遞任意的參數(shù)卻不造成錯誤茉继。那是因為函數(shù)參數(shù)實際上被保存在一個被稱為arguments的蕾絲數(shù)組的對象中。如果一個普通的javascirpt數(shù)組蚀乔,arguments可以自由增長來包含人一個數(shù)的值烁竭,這些值可通過數(shù)字索引來引用。arguments的length屬性會告訴你目前與有多少個值吉挣。
arguments對象自動存在於函數(shù)中派撕。也就是說,函數(shù)的命名參數(shù)不過是為了方便听想,並不真的限制了該函數(shù)可以接受參數(shù)的個數(shù)腥刹。
注意:arguments對象不是一個數(shù)組的實例马胧,其擁有的方法與數(shù)組不同汉买,array.isArray(arguments) 永遠返回false。
另一方面佩脊,javascript耶沒有忽視那些命名參數(shù)蛙粘。函數(shù)期望的參數(shù)個數(shù)保存在函數(shù)的length屬性中垫卤。還記得嗎?函數(shù)就是對象出牧,所有它可以有屬性穴肘。length屬性表明了該函數(shù)的期望參數(shù)個數(shù)。了解函數(shù)的期望參數(shù)個數(shù)在javascript中是非常重要的舔痕,因為給他傳遞過多或者過少的參數(shù)都不會拋出錯誤评抚。
下面是一個簡單的使用arguments和函數(shù)的期望參數(shù)個數(shù)的例子。注意實際傳入的參數(shù)的數(shù)量不影響函數(shù)的期望參數(shù)的個數(shù)伯复。
function reflect(value){
return value;
}
console.log(reflect("Hi!"));
console.log(reflect("Hi"),25);
?console.log(reflect.length);//1
reflect=function(){
return arguments[0];
};
console.log(reflect("Hi")); //hi
console.log(reflect("hi",25));
console.log(reflect.length);//0;
本例先定義了一個具有單一命名的參數(shù)reflect()函數(shù)慨代,但是當(dāng)有兩個參數(shù)傳遞給它時沒有任何錯誤發(fā)生。由於只有一個命名參數(shù)啸如,length屬性為1.代碼隨後重新定義reflect()為無命名參數(shù)的函數(shù)侍匙,它返回傳入的第一個參數(shù)arguments『0』。這個新版本的函數(shù)和前一個版本的輸出一模一樣叮雳,但是他的length為0想暗;
因為使用了命名參數(shù),reflect()的第一個實現(xiàn)容易理解(和在別的語言裡一樣)帘不。使用arguments對象的版本有點讓人莫名其妙说莫,因為沒有命名參數(shù),你不得不瀏覽整個函數(shù)體來確定是否使用了參數(shù)寞焙,這就是為什麼許多開發(fā)者盡可能避免arguments的原因唬滑。
不過,在某些情況下使用argumengs比命名參數(shù)更有效果棺弊。例如晶密,假設(shè)你想創(chuàng)建一個函數(shù)接受任意數(shù)量的參數(shù)並返回它們的和,因為你不知道會有多少個參數(shù)模她,所以你無法使用命名參數(shù)稻艰。在這種情況下,使用arguments是最好的選擇侈净。
function sum(){
var result=0,
i=0;
len=arguments.length;
while(i<len){
result+=arguments[i];
i++;
}
return result;
}
console.log(sum(1,2));
console.log(sum(3,4,5,6));
console.log(sum());//0
sum()函數(shù)接受任意數(shù)量的參數(shù)並在while循環(huán)中遍歷他們的值並求和尊勿。這就和對一個數(shù)組中的數(shù)字球和一樣。由於result出事值為0.該函數(shù)就算沒有參數(shù)也能正常工作畜侦。
2.4 重載
大多數(shù)面向?qū)ο笳Z言支持函數(shù)重載元扔,它能讓一個函數(shù)具有多個簽名。函數(shù)簽名由函數(shù)的名字旋膳,參數(shù)的個數(shù)及其類型組成澎语。因此,一個函數(shù)可以有一個接受一個字符串參數(shù)的簽名和另一個接受兩個數(shù)字參數(shù)的簽名。javascript語言根據(jù)實際傳入的參數(shù)決定調(diào)用函數(shù)的哪個版本擅羞。
之前已經(jīng)提過尸变,javascript函數(shù)可以接受任意數(shù)量的參數(shù)且參數(shù)類型完全沒有限制。這說明javascript函數(shù)其實根本沒有簽名减俏,因此也不存在重載召烂。看看當(dāng)你試圖聲明兩個同名函數(shù)會發(fā)生什么娃承。
javascript函數(shù)沒有簽名這個事實并不意味著你不能模仿函數(shù)重載奏夫。你可以用arguments對象獲取傳入的參數(shù)個數(shù)并決定怎么處理。例如:
function sayMessage(message){
if(arguments.length===0){
message="Default message";
}
console.log(message);
}
sayMessage("Hello");
本例中國年历筝,sayMessage()函數(shù)的行為視傳入?yún)?shù)的個數(shù)而定桶蛔。如果沒有傳入?yún)?shù),那么就使用默認的信息漫谷。否則使用第一個傳入的參數(shù)信息仔雷。和其他樹言中的重載相比。這里有更多的人為介入舔示。但是結(jié)果是相同的碟婆。如果你還想檢查不同的數(shù)據(jù)類型,你可以使用typeof和instanceof惕稻。
注意:在實際使用中竖共,檢查命名參數(shù)是否為未定義比依靠arguments .length 更常見。
?2.5 對象方法
第一章中介紹了可以在任何時候給對象添加或刪除屬性俺祠。如果屬性的值是函數(shù)公给,則該屬性被稱為方法。你可以像添加屬性那樣給對象添加方法蜘渣。例如淌铐,在下面的代碼中,變量person被賦予了一個對像的字面形式蔫缸,包含屬性name和方法sayName腿准。
var person={
name:"Nicholas",
sayName:function(){
console.log(person.name)
? ? ? ? };
};
person.sayName();
注意定義數(shù)據(jù)屬性和方法的愈發(fā)完全相同--標(biāo)示符后面跟著冒號和值拾碌。只不過sayName的值正好是一個函數(shù)吐葱。定義好以后你立刻就能在對象上調(diào)用方法person.sayName();
2.5.1 this對象
你可能已經(jīng)注意到前面的例子中一些奇怪之處。sayName()方法直接引用了person.name校翔。在方法和對象間建立了緊耦合弟跑。有太多理由證明這是有問題的。首先防症,如果你改變了變量名孟辑,你也必須要改變方法中引用的名字哎甲。其次,這種緊耦合使得痛一個方法很那被不同對象使用扑浸。幸好javascript對此有一個解決辦法。
javascript所有的函數(shù)作用域內(nèi)都有一個this對象代表調(diào)用函數(shù)的對象燕偶。在全局作用域中喝噪,this代表全局對象(瀏覽器里的window)。當(dāng)一個函數(shù)作為對象的方法被調(diào)用時指么,默認this的值等于那個對象酝惧。所以你應(yīng)該在方法內(nèi)引用this而不是直接飲用一個對象。前例代碼改寫如下伯诬。
var person={
name:"Nicholas",
sayName:function(){
console.log(this.name);
};
};
person.sayName();
這段代碼和前面的版本輸出相同晚唇,但是這一次,sayName()引用this而不是person盗似。這意味著你可以輕易改變變量名哩陕,甚至是將該函數(shù)用在不同對象上。
function sayNameForAll(){
console.log(this.name);
}
var person1={
name:"Nicholas",sayName:sayNameForAll};
var person2={
name:"Greg",
sayName:sayNameForAll
};
var name="Michael";
person1.sayName();
person2.sayName();
sayNameForAll();
本例先定義函數(shù)sayNameForAll,然后以字面形式創(chuàng)建兩個對象以sayNameForAll函數(shù)作為sayName方法赫舒。函數(shù)就是飲用值悍及,所以你可以把它們作為屬性值賦給任意個對象。當(dāng)person1調(diào)用sayName方法時接癌,輸出“Naicholas”心赶;person2則輸出“Greg”,那是因為this函數(shù)調(diào)用時才設(shè)置缺猛,所以this.name是正確的缨叫。
本例最后部分定義了全局變量name。全局變量被認為是全局對象的屬性荔燎,所以當(dāng)調(diào)用sayNameForAll時輸出"Michael".
2.5.2 改變this
在javascript中耻姥,使用和操作函數(shù)中this的能力時良好地面向?qū)ο缶幊痰年P(guān)鍵。函數(shù)會在各種不同上下文中被使用有咨,它們必須到哪里都能正常工作咏闪。一般this會被自動設(shè)置,但是你可以改變它們的值來完成不同的目標(biāo)摔吏。有3種函數(shù)方法允許你改變this的值鸽嫂。(記住函數(shù)時對象,而對象可以有方法征讲,所以函數(shù)也有)
1.call()方法
第一個用戶操作this的函數(shù)方法是call()据某,它以指定的this值和參數(shù)來執(zhí)行函數(shù)。call()的第一個參數(shù)制定了函數(shù)執(zhí)行時this的值诗箍,其后的所有參數(shù)都是需要被傳入函數(shù)的參數(shù)癣籽。假設(shè)你更新sayNameForAll讓它接受一個參數(shù),代碼如下:
function sayNameForAll(label){
console.log(label+":"+this.name);
}
var person1={
name:"Nicholas"
};
var person2={
name:"Greg"
};
var name ="Michael";
sayNameForAll.call(this,"global");//outputs "global:micahael"
sayNameForAll.call(person1,"person1");//outputs "person1:Nicholas"
sayNameForAll.call(person2,"person2");//outputs "person2:Greg"
在本例中,sayNameForAll()接受一個label參數(shù)用于輸出筷狼。然后該函數(shù)被調(diào)用3次瓶籽。注意調(diào)用函數(shù)時在函數(shù)名后沒有小括號,因為它被作為對象訪問而不是被執(zhí)行的代碼埂材。第一次調(diào)用使用全局this并傳入?yún)?shù)"global"來輸出“blobal:michael”塑顺。之后兩吃調(diào)用分別使用person1 和person2。由于使用了call()方法俏险,你不需要講函數(shù)加入每個對象--你顯式指定了this的值而不是javascript引擎自指定严拒。
2. apply()方法
apply()時你可以用來操作this的第二個函數(shù)方法。apply()的工作方式和call()完全一樣竖独,但它只接受兩個參數(shù):this的值和一個數(shù)組或者類似數(shù)組的對象裤唠,內(nèi)含需要被傳入函數(shù)的參數(shù)(也就是說你可以把arguments對象作為apply()的第二個參數(shù))。你不需要像使用call()那樣指定一個參數(shù)莹痢,而是可以輕松傳遞正個數(shù)組給apply()种蘸。除此之外,call()和apply()表現(xiàn)的完全一樣竞膳,下例演示了apply()的用法劈彪。
function sayNameForAll(label){
console.log(label+":"+this.name);
}
var person1={
name:"Nicholas"
};
var person2={
name:"Greg"
};
var name ="Michael";
sayNameForAll.call(this,[global]);//outputs "global:micahael"
sayNameForAll.call(person1,[person1]);//outputs "person1:Nicholas"
sayNameForAll.call(person2,[person2]);//outputs "person2:Greg"
這段代碼借用了前例并用apply()替換了call();結(jié)果完全相同顶猜。你通常會根據(jù)你手頭已有的數(shù)據(jù)決定使用哪個方法沧奴。如果哦哦你已經(jīng)有一個數(shù)組,你應(yīng)該用apple()长窄;如果你有的只是一個個單獨的變量滔吠,則用call()
3 bind()方法
改變this的第三個函數(shù)方法是bind()。ECMAScript5中添加的這個方法和之前的那兩個有些不同挠日。 按照慣例疮绷。bind()的第一個參數(shù)是要傳遞給新函數(shù)的this的值。其他所有參數(shù)代表需要唄永久設(shè)置在新函數(shù)中的命名參數(shù)嚣潜。你可以在之后繼續(xù)設(shè)置任何非永久參數(shù)冬骚。
下面代碼掩飾了兩個使用bind()的例子。創(chuàng)建sayNameForPerson1()函數(shù)并將person1綁定微其this對象的值懂算。然后創(chuàng)建sayNameForPerosn2()并將person2并定為其this對象的值只冻,“person2”綁定微其第一個參數(shù)。
function sayNameForAll(label){
console.log(label+":"+this.name);
}
var person ={
name:"Nicholas"
};
var person2={
name:"Greg"
};
//create a function just for person1
var sayNameForPerson1=sayNameForAll.bind(person1);
sayNameForPerson1("person1");//outputs "person1:"Nicholas
//create a function just for person2
var sayNameForPerson2=sayNameForAll.bind(person2,"person2");
sayNameForPerson2();
//attaching a method to an ?object doesn't change this
person2.sayName=sayNameForPerson1;
person2.sayName("person2");
sayNameForPerson1()沒有綁定參數(shù)计技,所以你仍然需要傳入label參數(shù)用于輸出喜德。sayNameForPerson2()不僅綁定this為person2,同時也綁定了第一個參數(shù)為“person2”垮媒。這意味著你可以調(diào)用sayNameForPerson2()而不傳入任何額外參數(shù)舍悯。
2.6 總結(jié)
javascript函數(shù)的獨特之處在于他們同時也是對象航棱,也就是說他們可以被訪問,復(fù)制和覆蓋萌衬,就像其他對象一樣饮醇。javascript中的函數(shù)和其他對象最大的區(qū)別在于他們有一個特殊的內(nèi)部屬性[[Call]],包含了該函數(shù)的執(zhí)行指令秕豫。typeof操作符會在對象內(nèi)查找這個內(nèi)部屬性朴艰,如果找到,它返回“function”馁蒂;
函數(shù)的字面形式有兩種種呵晚;聲明和表達式蜘腌。函數(shù)的聲明視function關(guān)鍵字右邊跟著函數(shù)名稱沫屡。函數(shù)聲明會被提升至上下文頂部。函數(shù)表達式可被用于任何使用值的地方撮珠,例如賦值語句沮脖,函數(shù)參數(shù)活另一個函數(shù)的返回值。
函數(shù)是對象芯急,所以存在一個function構(gòu)造函數(shù)勺届。你可以用function構(gòu)造函數(shù)創(chuàng)建新的函數(shù),不過沒有人會建議你這么做娶耍,因為它會使你的代碼難以理解和調(diào)試免姿。但是有時你可能不得不使用這個方法。例如在函數(shù)的真實形式直到運行時才能確定的時候榕酒。
為了理解javascript的面相對象編程胚膊。你需要好好理解它的函數(shù)。因為javascript沒有類的概念想鹰,能夠幫助你實現(xiàn)聚合和繼承的只有函數(shù)和其他對象了紊婉。