我們已經(jīng)在第1章討論過,在javascript中,函數(shù)其實就是對象,使函數(shù)不同意其他對象的決定性特點是函數(shù)存在一個被稱為[[Call]]的內(nèi)部屬性。內(nèi)部屬性無法通過代碼訪問而是定義了代碼執(zhí)行時的行為。ECMAScript為javascript的對象定義了多種內(nèi)部屬性,這些內(nèi)部屬性都用雙重中括號來標注。
?[[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ù)的範圍,要麼是全局範圍)的頂部,這意味著你可以先使用函數(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ù)。也可以將它們福祉給變量,在對象中添加它們,將它們當成參數(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ù)當成參數(shù)傳遞給其他的函數(shù)。javascript數(shù)組的sort()方法接受耶和華 i 個比較函數(shù)作為可選參數(shù),每當數(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ù),但是當有兩個參數(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ù)其實根本沒有簽名,因此也不存在重載。看看當你試圖聲明兩個同名函數(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ā)完全相同--標示符后面跟著冒號和值。只不過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)。當一個函數(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ù)就是飲用值,所以你可以把它們作為屬性值賦給任意個對象。當person1調(diào)用sayName方法時,輸出“Naicholas”;person2則輸出“Greg”,那是因為this函數(shù)調(diào)用時才設(shè)置,所以this.name是正確的。
本例最后部分定義了全局變量name。全局變量被認為是全局對象的屬性,所以當調(diào)用sayNameForAll時輸出"Michael".
2.5.2 改變this
在javascript中,使用和操作函數(shù)中this的能力時良好地面向?qū)ο缶幊痰年P(guān)鍵。函數(shù)會在各種不同上下文中被使用,它們必須到哪里都能正常工作。一般this會被自動設(shè)置,但是你可以改變它們的值來完成不同的目標。有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ù)和其他對象了。