第二章 函數

我們已經在第1章討論過,在javascript中,函數其實就是對象,使函數不同意其他對象的決定性特點是函數存在一個被稱為[[Call]]的內部屬性。內部屬性無法通過代碼訪問而是定義了代碼執行時的行為。ECMAScript為javascript的對象定義了多種內部屬性,這些內部屬性都用雙重中括號來標注。

?[[Call]]屬性是函數獨有的,表明該對象可以被執行。由於僅有函數擁有該屬性,ECMAScript定義typeof操作符對任何具有[[Call]]屬性的對象返回“funciton”。這在過去曾經導致了一些問題,因為某些瀏覽器曾經在正則表達式中包含了[[ Call]]屬性,導致後者被錯誤鑑定為函數,現在,多有瀏覽器的行為都一致,typeof不會將正則表達式鑒別為函數了。

2.1聲明還是表達式

函數具有兩種字面形式,第一種事函數聲明,以function關鍵字開頭,後面跟著函數的名字。函數的內容放在大括號內,例如下面就是函數聲明。


function add(num1,num2){return num1+num2}


第二種形式是函數表達式,funciton關鍵字後面不需要加上函數的名字。這種函數被稱為匿名函數,因為函數對象本身沒有名字。取而代之的函數表達式通常會被一個變量或者引用,下面就是函數表達式。

var add =function(num1,num2){

return num1+num2;

};

這段代碼時機上將一個函數作為值賦給變量add,除了沒有函數名並在最後多了一個分號以外,函數表達式幾乎和函數聲明完全一樣。函數表達式賦值通常在最後一個分號,就如同其他對像的賦值一樣。

雖然這兩種代碼形式頗為相似,但是他們有一個非常重要的區別,函數聲明會被提升至上下文(要麼是該函數被聲明時所在的函數的範圍,要麼是全局範圍)的頂部,這意味著你可以先使用函數後聲明他們。例如:

var result =add(5,5);

function add(num,num2){

return num1+num2;

}

這段代碼看上去似乎會造成錯誤,但實際上可以工總,那是因為javascript引擎將函數聲明提升至頂部來執行,就好像他被寫成如下形式:

//how the javascript engine interpers the code

function add(num1,num2){

return num1+num2;

}

var result=add(5,5);

javascript能對函數聲明進行提升,這是因為引擎提前知道了函數的名字。而函數表達式僅能通過變量引用,因此無法提升,所以下面這段代碼會導致錯誤

//error

var result =add(5,5);

var add=function(num1,num2){

return num1+num2;

};

只要你始終在使用函數之前定義他們,你就可以隨意使用函數聲明或表達式。

2.2 函數就是值

函數是javascript的一大重點,你可以像使用對象一樣使用函數。也可以將它們福祉給變量,在對象中添加它們,將它們當成參數傳遞給別的函數,或從別的函數中返回。基本上只要是可以使用其他引用值的地方,你就可以使用函數。這使得javascript的函數威力無窮,考慮下面的例子:

function sayHi(){

console.log("Hi");

}

sayHi();//outputs "hi";

var sayHi2=sayHi;

sayHi2();//outputs "hi";

這段代碼首先要有一個函數聲明sayHi。然後有一個變量sayHi2被創建並被賦予sayHi的值,sayHi和sayHi2現在指向同一個函數,兩者都可以被執行,並具有相同的結果。為了更好的理解這點,讓我來看一下用function構造函數重寫具有相同功能的代碼。

var sayHi=new Function("console.log(\"Hi!\");");

sayHi();

var sayHi2=sayHi;

sayHi2(); //outputs "Hi"

Function 構造函數更加清楚地表明sayHi能夠像其他對象一樣被傳來傳去。只要你記住函數就是對象,很多行為就變得容易理解了。

例如。你可以將函數當成參數傳遞給其他的函數。javascript數組的sort()方法接受耶和華 i 個比較函數作為可選參數,每當數組中兩個值需要進行比較時都會調用此函數。如果第一個值小於第二個。比較函數會返回一個負數。如果第一個值大於第二個,比較函數會返回一個正數 ,如果兩個值相等,函數返回0;

在默認情況下,sort()將數組中的每個對象轉換成字符串然後進行比較。這意味著,你無法在不指定比較函數的情況下為數字的數組進行精確排序。

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()的比較函數其實是一個函數表達式。請注意它沒有名字,僅作為引用被傳遞給另一個函數(著使得它被稱為匿名函數)。比較函數對兩個值進行比較相減法以返回正確的結果。

作為對比,第二次sort()不使用比較函數。結果和預期不太一樣,1後面跟著的事10.這是因為默認的比較函數將所有值都轉換成字符串比較。

2.3 參數

javascript函數的另一個獨特之處在於你可以給函數傳遞任意的參數卻不造成錯誤。那是因為函數參數實際上被保存在一個被稱為arguments的蕾絲數組的對象中。如果一個普通的javascirpt數組,arguments可以自由增長來包含人一個數的值,這些值可通過數字索引來引用。arguments的length屬性會告訴你目前與有多少個值。

arguments對象自動存在於函數中。也就是說,函數的命名參數不過是為了方便,並不真的限制了該函數可以接受參數的個數。


注意:arguments對象不是一個數組的實例,其擁有的方法與數組不同,array.isArray(arguments) 永遠返回false。


另一方面,javascript耶沒有忽視那些命名參數。函數期望的參數個數保存在函數的length屬性中。還記得嗎?函數就是對象,所有它可以有屬性。length屬性表明了該函數的期望參數個數。了解函數的期望參數個數在javascript中是非常重要的,因為給他傳遞過多或者過少的參數都不會拋出錯誤。

下面是一個簡單的使用arguments和函數的期望參數個數的例子。注意實際傳入的參數的數量不影響函數的期望參數的個數。


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;


本例先定義了一個具有單一命名的參數reflect()函數,但是當有兩個參數傳遞給它時沒有任何錯誤發生。由於只有一個命名參數,length屬性為1.代碼隨後重新定義reflect()為無命名參數的函數,它返回傳入的第一個參數arguments『0』。這個新版本的函數和前一個版本的輸出一模一樣,但是他的length為0;

因為使用了命名參數,reflect()的第一個實現容易理解(和在別的語言裡一樣)。使用arguments對象的版本有點讓人莫名其妙,因為沒有命名參數,你不得不瀏覽整個函數體來確定是否使用了參數,這就是為什麼許多開發者盡可能避免arguments的原因。

不過,在某些情況下使用argumengs比命名參數更有效果。例如,假設你想創建一個函數接受任意數量的參數並返回它們的和,因為你不知道會有多少個參數,所以你無法使用命名參數。在這種情況下,使用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()函數接受任意數量的參數並在while循環中遍歷他們的值並求和。這就和對一個數組中的數字球和一樣。由於result出事值為0.該函數就算沒有參數也能正常工作。

2.4 重載

大多數面向對象語言支持函數重載,它能讓一個函數具有多個簽名。函數簽名由函數的名字,參數的個數及其類型組成。因此,一個函數可以有一個接受一個字符串參數的簽名和另一個接受兩個數字參數的簽名。javascript語言根據實際傳入的參數決定調用函數的哪個版本。

之前已經提過,javascript函數可以接受任意數量的參數且參數類型完全沒有限制。這說明javascript函數其實根本沒有簽名,因此也不存在重載。看看當你試圖聲明兩個同名函數會發生什么。

javascript函數沒有簽名這個事實并不意味著你不能模仿函數重載。你可以用arguments對象獲取傳入的參數個數并決定怎么處理。例如:

function sayMessage(message){

if(arguments.length===0){

message="Default message";

}

console.log(message);

}

sayMessage("Hello");

本例中國年,sayMessage()函數的行為視傳入參數的個數而定。如果沒有傳入參數,那么就使用默認的信息。否則使用第一個傳入的參數信息。和其他樹言中的重載相比。這里有更多的人為介入。但是結果是相同的。如果你還想檢查不同的數據類型,你可以使用typeof和instanceof。

注意:在實際使用中,檢查命名參數是否為未定義比依靠arguments .length 更常見。

?2.5 對象方法

第一章中介紹了可以在任何時候給對象添加或刪除屬性。如果屬性的值是函數,則該屬性被稱為方法。你可以像添加屬性那樣給對象添加方法。例如,在下面的代碼中,變量person被賦予了一個對像的字面形式,包含屬性name和方法sayName。

var person={

name:"Nicholas",

sayName:function(){

console.log(person.name)

? ? ? ? };

};

person.sayName();

注意定義數據屬性和方法的愈發完全相同--標示符后面跟著冒號和值。只不過sayName的值正好是一個函數。定義好以后你立刻就能在對象上調用方法person.sayName();

2.5.1 this對象

你可能已經注意到前面的例子中一些奇怪之處。sayName()方法直接引用了person.name。在方法和對象間建立了緊耦合。有太多理由證明這是有問題的。首先,如果你改變了變量名,你也必須要改變方法中引用的名字。其次,這種緊耦合使得痛一個方法很那被不同對象使用。幸好javascript對此有一個解決辦法。

javascript所有的函數作用域內都有一個this對象代表調用函數的對象。在全局作用域中,this代表全局對象(瀏覽器里的window)。當一個函數作為對象的方法被調用時,默認this的值等于那個對象。所以你應該在方法內引用this而不是直接飲用一個對象。前例代碼改寫如下。

var person={

name:"Nicholas",

sayName:function(){

console.log(this.name);

};

};

person.sayName();

這段代碼和前面的版本輸出相同,但是這一次,sayName()引用this而不是person。這意味著你可以輕易改變變量名,甚至是將該函數用在不同對象上。

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();

本例先定義函數sayNameForAll,然后以字面形式創建兩個對象以sayNameForAll函數作為sayName方法。函數就是飲用值,所以你可以把它們作為屬性值賦給任意個對象。當person1調用sayName方法時,輸出“Naicholas”;person2則輸出“Greg”,那是因為this函數調用時才設置,所以this.name是正確的。

本例最后部分定義了全局變量name。全局變量被認為是全局對象的屬性,所以當調用sayNameForAll時輸出"Michael".

2.5.2 改變this

在javascript中,使用和操作函數中this的能力時良好地面向對象編程的關鍵。函數會在各種不同上下文中被使用,它們必須到哪里都能正常工作。一般this會被自動設置,但是你可以改變它們的值來完成不同的目標。有3種函數方法允許你改變this的值。(記住函數時對象,而對象可以有方法,所以函數也有)

1.call()方法

第一個用戶操作this的函數方法是call(),它以指定的this值和參數來執行函數。call()的第一個參數制定了函數執行時this的值,其后的所有參數都是需要被傳入函數的參數。假設你更新sayNameForAll讓它接受一個參數,代碼如下:

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參數用于輸出。然后該函數被調用3次。注意調用函數時在函數名后沒有小括號,因為它被作為對象訪問而不是被執行的代碼。第一次調用使用全局this并傳入參數"global"來輸出“blobal:michael”。之后兩吃調用分別使用person1 和person2。由于使用了call()方法,你不需要講函數加入每個對象--你顯式指定了this的值而不是javascript引擎自指定。

2. apply()方法

apply()時你可以用來操作this的第二個函數方法。apply()的工作方式和call()完全一樣,但它只接受兩個參數:this的值和一個數組或者類似數組的對象,內含需要被傳入函數的參數(也就是說你可以把arguments對象作為apply()的第二個參數)。你不需要像使用call()那樣指定一個參數,而是可以輕松傳遞正個數組給apply()。除此之外,call()和apply()表現的完全一樣,下例演示了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();結果完全相同。你通常會根據你手頭已有的數據決定使用哪個方法。如果哦哦你已經有一個數組,你應該用apple();如果你有的只是一個個單獨的變量,則用call()

3 bind()方法

改變this的第三個函數方法是bind()。ECMAScript5中添加的這個方法和之前的那兩個有些不同。 按照慣例。bind()的第一個參數是要傳遞給新函數的this的值。其他所有參數代表需要唄永久設置在新函數中的命名參數。你可以在之后繼續設置任何非永久參數。

下面代碼掩飾了兩個使用bind()的例子。創建sayNameForPerson1()函數并將person1綁定微其this對象的值。然后創建sayNameForPerosn2()并將person2并定為其this對象的值,“person2”綁定微其第一個參數。

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()沒有綁定參數,所以你仍然需要傳入label參數用于輸出。sayNameForPerson2()不僅綁定this為person2,同時也綁定了第一個參數為“person2”。這意味著你可以調用sayNameForPerson2()而不傳入任何額外參數。

2.6 總結

javascript函數的獨特之處在于他們同時也是對象,也就是說他們可以被訪問,復制和覆蓋,就像其他對象一樣。javascript中的函數和其他對象最大的區別在于他們有一個特殊的內部屬性[[Call]],包含了該函數的執行指令。typeof操作符會在對象內查找這個內部屬性,如果找到,它返回“function”;

函數的字面形式有兩種種;聲明和表達式。函數的聲明視function關鍵字右邊跟著函數名稱。函數聲明會被提升至上下文頂部。函數表達式可被用于任何使用值的地方,例如賦值語句,函數參數活另一個函數的返回值。

函數是對象,所以存在一個function構造函數。你可以用function構造函數創建新的函數,不過沒有人會建議你這么做,因為它會使你的代碼難以理解和調試。但是有時你可能不得不使用這個方法。例如在函數的真實形式直到運行時才能確定的時候。

為了理解javascript的面相對象編程。你需要好好理解它的函數。因為javascript沒有類的概念,能夠幫助你實現聚合和繼承的只有函數和其他對象了。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 也許你還沒有理解構造函數和原型對象的時候已經在javascript的路上走了很久,但直到你很好的掌握它們之前你不會...
    WanLum閱讀 417評論 0 1
  • 盡管javascript里有大量內建引用對象,很可能你還說會頻繁創建自己的對象。當你在這么做的時候,記得javas...
    WanLum閱讀 540評論 1 3
  • 學習如何創建對象時理解面向對象的第一步。第二部時理解繼承。在傳統面向對象的語言中,類從其他類繼承屬性。然而在jav...
    WanLum閱讀 270評論 0 0
  • javascript有很多創建對象的模式,完成工作的方式也不只一種。你可以隨時定義自己的類型或自己的泛用對象。可以...
    WanLum閱讀 269評論 0 0
  • 為何叫做 shell ? shell prompt(PS1) 與 Carriage Return(CR) 的關系?...
    Zero___閱讀 3,185評論 3 49