在 JavaScript 中,函式是物件,是 Function
的實例。因為是 Function
實例,你可以將之傳給另一個變數參考。例如:
function max(num1, num2) {
return num1 > num2 ? num1 : num2;
}
var maximum = max;
console.log(max(10, 20)); // 20
console.log(maximum(10, 20)); // 20
使用 function
宣告函式,函式名稱就相當於使用 var
宣告了變數名稱,嚴格模式下不可使用 delete
來刪除,在上例中,如果試圖 delete max
,會引發 SyntaxError
。
注意,在將 max
指定給 maximum
時,max
後並沒有加上 ()
運算子,這表示要將 max
參考的物件指定給 maximum
參考(加上括號表示要執行函式)。將一個函式指定給變數,就像將一個數字指定給一個變數一樣,這看來如果覺得奇怪的話,或許下這個看來比較不奇怪:
var max = function(num1, num2) {
return num1 > num2 ? num1 : num2;
};
var maximum = max;
console.log(max(10, 20)); // 20
console.log(maximum(10, 20)); // 20
函式就如同數值,可以指定給變數,函式與數值的地位相同,並不會像有些語言中,無法像數值一樣地被指定,不會淪為二等公民,因此,對於支持函式可如數值一樣指定給變數的語言,我們稱函式在這個語言中是一等(First-class)函式或一級函式。
上面你所看到的函式撰寫方式,稱之為函式實字(Function literal),這就像你寫下一個數值實字、物件實字或陣列實字,會產生數值或物件等:
var number = 10; // Number literal
var obj = {x : 10}; // Object literal
var array = [1, 2, 3]; // Array literal
var func = function() { // Function literal
// do something...
};
函式實字會產生 Function
實例,在 JavaScript 中,無論是函式宣告或函式實字,都會產生 Function
實例。事實上,你也可以直接指定建立 Function
實例:
var max = new Function('num1', 'num2',
'return num1 > num2 ? num1 : num2'
);
var maximum = max;
console.log(max(10, 20)); // 20
console.log(maximum(10, 20)); // 20
基本上,實務上很少會直接建立 Function
實例,以上只是表示,函式確實是 Function
實例。
既然函式是物件,它就可以任意指定給其他變數,也就可以指定作為另一個函式的引數,那它就不僅能被呼叫,還可以主動要求另一個函式執行所指定的函式內容。例如:
var printIt = function(elem) {
console.log(elem);
};
[1, 2, 3].forEach(printIt); // 1 2 3
var naturalOrder = function(num1, num2) {
return num1 - num2;
};
[5, 1, 7, 3, 2].sort(naturalOrder) // 1 2 3 5 7
.forEach(printIt);
上例以 Array
為例,forEach
可以對陣列的每個元素作「某些事」,「某些事」是由你使用函式來指定,陣列會逐一將元素傳入給你指定的函式作為引數。
sort
則可以進行排序,但兩個元素的大小關係要由你告知,傳回正值表示傳入的 num1
順序上大於 num2
,要排在 num2
的後面,傳回 0 表示兩個順序相同,傳回負值表示 num1
順序上小於 num2
,要排在 num2
的前面。
像這種將函式主動丟入函式作為引數,在 JavaScript 等具備一級函式的語言中,是很常見到的應用。事實上,若不需要名稱,你也可以如下:
var numbers = [5, 1, 7, 3, 2];
numbers.sort(function(num1, num2) { // 1 2 3 5 7
return num1 - num2;
})
.forEach(function(elem) {
console.log(elem);
});
直接傳入函式很方便,不過函式名稱有時是必要的,像上面的可讀性並不會比較好。
你也可以從函式中傳回函式,這通常會形成閉包(Closure)綁定某些運算過後的資源,再傳回函式,這之後還會再談到應用。
以函式實字所建立的 Function
實例,在指定給別的變數前,稱為所謂的匿名函式(Anonymous function)。你可以完全不使用名稱來執行函式:
(function() {
console.log('匿名函式...');
})();
實際上,函式實字也可以指定名稱。例如:
var maximum = function max(num1, num2) {
return num1 > num2 ? num1 : num2;
};
console.log(maximum(10, 20)); // 20
console.log(max(10, 20)); // ReferenceError: max is not defined
上例中,函式實字所建立的 max
名稱,似乎不能使用,事實上,這種語法適用於使用函式實字建立 Function
實例,但又需遞迴的場合。例如:
var gcd = function g(num1, num2) {
return num2 != 0 ? g(num2, num1 % num2) : num1;
};
console.log(gcd(10, 5)); // 5
函式既然是物件,本身亦可擁有特性。例如函式有個 length
特性,代表其參數個數:
var gcd = function g(num1, num2) {
return num2 != 0 ? g(num2, num1 % num2) : num1;
};
console.log(gcd.length); // 2
函式也可以擁有方法,這個之後再談,你也可以在其上新增特性或方法,就如同一個普通的物件。
函式宣告與函式實字在運用上,幾乎是相同的,但還是有細微的差別。例如,以下可以正常執行:
func();
function func() {
console.log('func');
}
不過以下會發生 TypeError
:
func(); // TypeError: undefined is not a function
var func = function() {
console.log('func');
};
錯誤訊息告訴你,func
值是 undefined
。原因在於,直譯器在載入 .js 時,會先處理所有的宣告,包括變數與函式宣告,接著再執行程式。所以在第一個範例中,是以函式宣告方式,直譯器已處理完成,因此接下來再執行時,就可以找到 func
所參考的函式。
而在第二個程式中,僅宣告了 func
變數,直譯器處理完這個宣告後,接下來要執行程式時,範圍中可以看到 func
變數,但此時還沒有指定值給 func
,所以是 undefined
,因此不能完成函式的執行。
雖然不重要,但還是提一下的是,以上兩種方式,在遇到要建立函式實例時,都不會再重新直譯,但如果你以直接建構 Function
實例的方式,則每次都會針對你引數的字串再作直譯動作。
var max = new Function('num1', 'num2',
'return num1 > num2 ? num1 : num2'
);