在ECMAScript中函數實際上是對象。每一個函數都是Function類型的實例,而且都與其他援用類型1樣具有屬性和方法。由于函數是對象,因此函數名實際上也是1個指向函數對象的指針,不會與某個函數綁定。函數通常是使用函數聲明語法定義的,如:
function sum(num1 , num2){
returnnum1 + num2;
}
這與下面使用函數表達式定義函數的方式幾近相差無幾:
var sum = function(num1 , num2){
returnnum1 + num2;
};
以上代碼定義了變量sum并將其初始化為1個函數,上面的例子中function關鍵字后面沒有函數名。這是由于在使用函數表達式定義函數時,沒有必要使用函數名——通常變量sum便可以援用函數。另外,還要注意函數末尾有1個分號,就像聲明其他變量1樣。
最后1種定義函數的方式是使用Function構造函數。Function構造函數可以接收任意數量的參數,但最后1個參數始終都被看成是函數體,而前面的參數則枚舉出了新函數的參數,如:
var sum = new Function(“num1” , “num2” , “returnnum1 + num2”);//不推薦
從技術角度講,這是1個函數表達式。但是,不推薦使用這類方法定義函數,由于這類語法會致使解析兩次代碼(第1次是解析常規ECMAScript代碼,第2次是解析傳入構造函數中的字符串),從而影響性能。不過,這類語法對理解“函數是對象,函數名是指針”的概念倒是非常直觀的。
由于函數名僅僅是履行函數的指針,因此函數名與包括對象指針的其他變量沒有甚么不同。換句話說,1個函數可能會有多個名字。
將函數名想象為指針,也有助于理解為何ECMAScript中沒有函數重載的概念,聲明多個同名的函數,即便傳入的參數的個數不1樣,該名字也只屬于最后1個函數。
解析器在向履行環境中加載數據時,會率先讀取函數聲明,并使其在履行任何代碼之前可用(可以訪問);至于函數表達式,則必須等到解析器履行到它所在的代碼行,才會真正被解釋履行,如:
alert(sum(10 , 20));//30
function sum(num1 , num2){
return num1 + num2;
}
//alert(add(10 , 20));//報錯,停止向下解析履行
var add = function(sum1 , sum2){
return sum1 + sum2;
};
在上面的例子中分別使用函數聲明和函數表達式定義了兩個函數sum(num1 , num2)和add(sum1 , sum2),一樣是函數,但是調用的結果卻大相徑庭。在sum前調用sum函數,能夠正常履行,也就是在sum聲明朝碼行前調用該函數,當前環境中即存在該函數;但是對add函數,在聲明朝碼行對其進行調用卻報錯了。
接下來將調用代碼調劑到add函數聲明的后面:
var add = function(sum1 , sum2){
return sum1 + sum2;
};
alert(add(10 , 20));//30
此時的結果就正常了,從上面的對照中可以得出這樣的結論,使用第1種方式聲明的函數會在代碼解析前被解析到當前環境中,而使用第2種方式聲明的函數只有在解析器解析到對應的代碼行時才會在當前環境中存在,此時看下面的例子就比較容易理解了:
function sum(num1 , num2){
return "第1個sum函數";
}
alert(sum(10, 20));//第3個sum函數
var sum = function(sum1 , sum2){
return "第2個sum函數";
};
function sum(num1 , num2){
return "第3個sum函數";
}
alert(sum(10 , 20));//第2個sum函數
上面的例子中聲明了3個同名的函數,上面說到,JavaScript中沒有重載的概念,函數名屬于最后1個聲明的函數實例。對上面代碼中的第1個alert(),這個結論沒有錯,但是對最后的alert(),明顯sum這個函數名指向的是第2個函數實例,造成這樣的結果是由于第2個函數實例是最后1個被解析的,也就是說,環境中終究的sum變量指向了第2個函數實例。因此,此時就能夠將上面的結論修改成:在JavaScript中沒有重載的概念,多個重名的函數聲明,該函數名屬于最后1個被解析的函數實例。
也能夠同時使用函數聲明和函數表達式,如varsum = function sum(num1 , num2){},與單獨使用函數表達式是等價的。不過,這類語法在Safari中會致使毛病。
由于ECMAScript中的函數名本身就是變量,所以函數也能夠作為值來使用。也就是說,不但可以像傳遞參數1樣把1個函數傳遞給另外一個函數,而且可以將1個函數作為另外一個函數的結果返回。如:
//將1個函數作為參數傳遞給另外一個函數
function add(num1 , num2){
return num1 + num2;
}
function raise(add , num1 , num2){
return add(num1 , num2)+1;
}
alert(raise(add, 10 , 10));//21
在Array類型中,其迭代方法的情勢與上例中1致,如:
var numbers = [0,1,2,3];
var everyResult = numbers.every(function(item , index , array){
return (item > 2);
});
alert(everyResult);//false
var someResult = numbers.some(function(item , index , array){
return (item > 2);
});
alert(someResult);//true
將函數作為返回值返回是1種極其有用的技術,例如在數組排序時需要項sort()方法中傳入1個比較函數,如果想在傳入的函數中指定排序的方式,則可以以下進行操作:
function compare(asc){
alert(asc);
if(asc){
return function(value1 , value2){
if(value1 < value2){
return ⑴;
} else if(value1 > value2){
return 1;
}else{
return 0;
}
};
}else{
return function(value1 , value2){
if(value1 > value2){
return ⑴;
} else if(value1 < value2){
return 1;
}else{
return 0;
}
};
}
}
var array = [0,5,1,15,10];
array.sort(compare(false));
alert(array);
在compare函數中根據指定的asc屬性判斷返回降序排列的函數還是升序排列的函數。
在函數內部,有兩個特殊的對象:arguments和this。其中arguments是1個類數組對象,包括著函數中的所有參數。雖然arguments的主要用處是保存函數參數,但這個對象還有1個名叫callee的屬性,該屬性是1個指針,指向具有這個arguments對象的函數,看下面的經典的階乘函數:
function factorial(num){
if(num <=1){
return 1;
}else{
return num*factorial(num⑴);
}
}
alert(factorial(5));//120
定義階乘函數1般都要用到遞歸算法;如上面的代碼所示,在函數著名字,而且名字以后也不會變的情況下,這樣定義沒有問題。但問題是這個函數的履行與函數名factorial牢牢耦合在了1起。為了消除這類緊密耦合的現象,可以像下面這樣使用arguments.callee:
function factorial(num){
if(num <=1){
return 1;
}else{
return num*arguments.callee(num⑴);
}
}
alert(factorial(5));//120
這個重寫后的factorial()函數的函數體內,沒有再援用函數名factorial。這樣,不管援用函數時使用的是甚么名字,都可以保證正常完成遞歸調用,例如:
function factorial(num){
if(num <=1){
return 1;
}else{
return num*arguments.callee(num⑴);
}
}
var trueFactorial = factorial;
factorial = function(){
return 0;
};
alert(trueFactorial(5));//120
alert(factorial(5));//0
在此,變量trueFactorial取得了factorial的值,實際上是在另外一個位置上保存了1個函數的指針。然后,我們又將1個簡單地返回0的函數賦值給factorial變量。如果像原來的factorial()那樣不使用arguments.callee,調用trueFactorial()就會返回0。可是,在消除了函數體內的代碼與函數名的耦合狀態以后,trueFactorial()依然能夠正常地計算階乘;至于factorial(),它現在只是1個返回0的函數。
函數內部的另外一個特殊對象是this,其行動與Java和C#中的this大致類似。換句話說,this援用的是函數履行的環境對象——或也能夠說是this值(當在網頁的全局作用域中調用函數時this對象援用的就是winsow)。如:
console.log(this);//window
window.hello = "hello world";
alert(this.hello);//hello world
ECMAScript 5 也規范化了另外一個函數對象的屬性:caller。除Opera的初期版本不支持,其他閱讀器都支持這個ECMAScript3并沒有定義的屬性。這個屬性中保存著調用當前函數的函數的援用,如果是在全局作用域中調用當前函數,它的值為null。如:
function outer(){
inner();
}
function inner(){
alert(inner.caller);
}
outer();
上面的的代碼會致使正告框中顯示outer()函數的源代碼。由于outer()調用了inner(),所以inner.caller就指向outer()。為了實現更疏松的耦合,也能夠通過arguments.callee.caller來訪問一樣的信息。
function outer(){
inner();
}
function inner(){
alert(arguments.callee.caller);
}
outer();
IE、Firefox、Chrome和Safari的所有版本和Opera9.6都遲滯caller屬性。
當函數在嚴格模式下運行時,訪問arguments.callee會致使毛病。ECMAScript5還定義了arguments.caller屬性,但在嚴格模式下訪問它也會致使毛病,而在非嚴格模式下這個屬性始終是undefined。定義這個屬性是為了分清arguments.caller和函數的caller屬性。以上變化否是為了加強這門語言的安全性,這樣第3方代碼就不能在相同的環境里窺視其它代碼了。
嚴格模式還有1個限制:不能為函數的caller屬性賦值,否則會致使毛病。
ECMAScript中的函數是對象,因此函數也有屬性和方法。每一個函數都包括兩個屬性length和prototype。其中length屬性表示函數希望接收的命名參數的個數。
在ECMAScript核心所定義的全部屬性中,最耐人尋味的就要數prototype屬性了。對ECMAScript中的援用類型而言,proptotype是保存它們所有實例方法的真正所在。換句話說,諸如toString()和valueOf()等方法實際上都保存在prototype名下,只不過是通過各自對象的實例訪問罷了。在創建自定義援用類型即實現繼承時,prototype屬性的作用是極其重要的。在ECMAScript5中,prototype屬性是不可枚舉的,因此使用for-in沒法發現。
沒個函數都包括兩個非繼承而來的方法:apply()和call(),這兩個方法的用處都是在特定的作用域中調用函數,實際上等于設置函數體內this對象的值。首先,apply()方法接收兩個參數:1個是在其中運行函數的作用域,另外一個是參數數組。其中,第2個參數可以是Array的實例,也能夠是arguments對象。如:
function sum(num1 , num2){
return num1 + num2;
}
function callSum1(num1 , num2){
return sum.apply(this , arguments);//傳入arguments對象
}
function callSum2(num1 , num2){
return sum.apply(this , [num1 , num2]);//傳入數組
}
alert(callSum1(10,10));//20
alert(callSum2(10,10));//20
在上面這個例子中,callSum1()在履行sum()函數時傳入了this作為this值(由于是在全局作用域中調用的,所以傳入的就是window對象)和arguments對象。而callSum2一樣也調用了sum()函數,但它傳入的則是this和1個參數數組。這兩個函數都會正常履行并返回正確的結果。
在嚴格模式下,為指定環境對象而調用函數,則this只不會轉型為window。除非把函數添加到某個對象或調用apply()或call(),否則this值將是undefined。
call()方法與apply()方法的作用相同,它們的區分僅在于接收參數的方式不同。對call()方法而言,第1個參數是this值沒有變化,變化的是其余參數都是直接傳遞給函數。換句話說,在使用call()方法時,傳遞給函數的參數必須逐一羅列出來,如:
function sum(num1 , num2){
return num1 + num2;
}
function callSum1(num1 , num2){
return sum.call(this , num1 , num2);
}
alert(callSum1(10,10));//20
在使用call()方法的情況下,callSum()必須明確地傳入每個參數。結果與使用apply()沒有甚么不同。至因而使用apply()還是call(),完全取決于你采取哪一種給函數傳遞參數的方式最方便。
事實上,傳遞參數并不是apply()和call()真實的用武之地;它們真正強大的地方是能夠擴充函數賴以運行的作用域。如:
window.color = "red";
var o = {color:"blue"};
function sayColor(){
alert(this.color);
}
sayColor();//red
sayColor.call(this);//red
sayColor.call(window);//red
sayColor.call(o);//blue
這個例子中sayColor()是作為全局函數定義的,而且當在全局作用域中調用它時,它確切會顯示”red”——由于this.color的值會轉換成對window.color的求值。而sayColor.call(this)和sayColor.call(window),則是兩種顯式地在全局作用域中調用函數的方式,結果固然都會顯示”red”。但是,當運行sayColor.call(o)時,函數的履行環境就不1樣了,由于此時函數體內的this對象指向了o,因而結果顯示的是”blue”。
使用call()或(apply())來擴充作用域的最大好處就是對象不需要與方法有任何耦合關系。在前面例子的第1個版本vzhong,先將sayColor()函數放到了對象o中,然后再通過o來調用它們的;而在這里重寫的例子中,就不需要先前那個過剩的步驟了。
ECMAScript5還定義了1個方法bind()。這個方法會創建1個函數的實例,其this值會被綁定到傳給bind()函數的值。如:
window.color = "red";
var o = {color:"blue"};
function sayColor(){
alert(this.color);
}
var objectSayColor = sayColor.bind(o);
objectSayColor();//blue
在這里,sayColor()調用bind()并傳入對象o,創建了objectSayColor()函數。objectSayColor()函數的this等于o,因此即便是在全局作用域中調用這個函數,也會看到”blue”。
支持bind()方法的閱讀器有IE9+、Firefox4+、Safari5.1+、Opera12+和Chrome。
每一個函數繼承的toLocaleString()和toString()方法始終都返回函數的代碼。返回代碼的格式因閱讀器而異。