初探一級函式

July 24, 2022

在 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'
);

分享到 LinkedIn 分享到 Facebook 分享到 Twitter