第二章 函數
我們已經在第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没有类的概念,能够帮助你实现聚合和继承的只有函数和其他对象了。