第 15 章函數
目錄
購買書籍
(廣告,請勿封鎖。)

第 15 章函數

函數是可以呼叫的值。定義函數的方法之一稱為函數宣告例如,下列程式碼定義函數 id,它有一個參數 x

function id(x) {
    return x;
}

return 陳述式會傳回 id 的值。您可以透過提及函數名稱,後接括號中的引數來呼叫函數:

> id('hello')
'hello'

如果您沒有從函數傳回任何內容,則會(隱式地)傳回 undefined

> function f() { }
> f()
undefined

本節只展示定義函數和呼叫函數的一種方法。其他方法會在後續說明。

函數在 JavaScript 中的三個角色

一旦您定義好函數,如前所示,它可以扮演幾個角色:

非方法函數(「一般函數」)

您可以直接呼叫函數。然後它會作為一般函數運作。以下是呼叫範例:

id('hello')

根據慣例,一般函數的名稱以小寫字母開頭。

建構函數

您可以透過 new 算子呼叫函數。然後它會變成建構函數,一個物件工廠。以下是呼叫範例:

new Date()

根據慣例,建構函數的名稱以大寫字母開頭。

方法

您可以將函數儲存在物件的屬性中,這會將它變成方法,您可以透過該物件呼叫它。以下是呼叫範例:

obj.method()

根據慣例,方法的名稱以小寫字母開頭。

非方法函數會在本章說明;建構函數和方法會在第 17 章說明。

定義函數

本節說明建立函數的三種方法:

  • 透過函數表達式
  • 透過函數宣告
  • 透過建構函式 Function()

所有函數都是物件,Function 的執行個體

function id(x) {
    return x;
}
console.log(id instanceof Function); // true

因此,函數從 Function.prototype 取得其方法。

函數表達式

函數表達式產生一個值,也就是函數物件。例如:

var add = function (x, y) { return x + y };
console.log(add(2, 3)); // 5

前述程式碼將函數表達式的結果指定給變數 add,並透過該變數呼叫它。函數表達式產生的值可以指定給變數(如最後一個範例所示),傳遞為另一個函數的引數,等等。由於一般函數表達式沒有名稱,因此它們也稱為 匿名函數表達式

命名函數表達式

您可以為函數表達式命名。 命名函數表達式 允許函數表達式參照它自己,這對於自我遞迴很有用:

var fac = function me(n) {
    if (n > 0) {
        return n * me(n-1);
    } else {
        return 1;
    }
};
console.log(fac(3)); // 6

注意

命名函數表達式的名稱只能在函數表達式內使用

var repeat = function me(n, str) {
    return n > 0 ? str + me(n-1, str) : '';
};
console.log(repeat(3, 'Yeah')); // YeahYeahYeah
console.log(me); // ReferenceError: me is not defined

函數宣告

以下是 函數宣告:

function add(x, y) {
    return x + y;
}

前述內容看起來像函數表達式,但它是一個陳述式(請參閱 表達式與陳述式)。它大致等於下列程式碼

var add = function (x, y) {
    return x + y;
};

換句話說,函數宣告宣告一個新變數,建立一個函數物件,並將其指定給變數。

函數建構函式

建構函式 Function() 會評估儲存在字串中的 JavaScript 程式碼。例如,下列程式碼等同於前一個範例:

var add = new Function('x', 'y', 'return x + y');

不過,這種定義函式的寫法很慢,而且會將程式碼保留在字串中(工具無法存取)。因此,如果可行,最好使用函式運算式或函式宣告。 使用 new Function() 評估程式碼 會更詳細地說明 Function();它的運作方式類似於 eval()

提升

提升 意指「移到範圍的開頭」。函式宣告會完全提升,變數宣告只會部分提升。

函式宣告會完全提升。這讓您可以在宣告函式之前呼叫它

foo();
function foo() {  // this function is hoisted
    ...
}

前述程式碼之所以會運作,是因為 JavaScript 引擎會將 foo 的宣告移到範圍的開頭。它們執行程式碼時,就像程式碼看起來像這樣

function foo() {
    ...
}
foo();

var 宣告也會提升,但只有宣告會提升,不會提升使用宣告進行的指定。因此,使用 var 宣告和函式運算式,類似於前一個範例,會導致錯誤:

foo();  // TypeError: undefined is not a function
var foo = function () {
    ...
};

只有變數宣告會提升。引擎會將前述程式碼執行為

var foo;
foo();  // TypeError: undefined is not a function
foo = function () {
    ...
};

函式的名稱

大多數 JavaScript 引擎支援函式物件的非標準屬性 name函式宣告有這個屬性:

> function f1() {}
> f1.name
'f1'

匿名的函式運算式的名稱是空字串:

> var f2 = function () {};
> f2.name
''

不過,有名稱的函式運算式有名稱:

> var f3 = function myName() {};
> f3.name
'myName'

函式名稱有助於偵錯。有些人會因此始終為函式運算式命名。

函式宣告或函式運算式:哪個比較好?

您應該偏好下列哪一種函式宣告?

function id(x) {
    return x;
}

還是 var 宣告加上函式運算式的等效組合?

var id = function (x) {
    return x;
};

它們基本上相同,但函式宣告相較於函式表達式有兩個優點:

  • 它們會被提升(請參閱提升),因此您可以在它們出現在原始碼之前呼叫它們。
  • 它們有一個名稱(請參閱函式的名稱)。不過,JavaScript 引擎越來越能推斷匿名函式表達式的名稱。

更能控制函式呼叫:call()、apply() 和 bind()

call()apply()bind() 是所有函式都具有的方法(請記住,函式是物件,因此具有方法)。它們可以在呼叫方法時提供 this 的值,因此主要在物件導向的環境中令人感興趣(請參閱設定 this 時呼叫函式:call()、apply() 和 bind())。本節說明非方法的兩個使用案例。

func.apply(thisValue, argArray)

這個方法在呼叫函式 func 時使用 argArray 的元素作為引數;亦即,下列兩個表達式是等效的:

func(arg1, arg2, arg3)
func.apply(null, [arg1, arg2, arg3])

thisValue 是執行 functhis 的值。在非物件導向的設定中不需要它,因此這裡是 null

apply() 在函式以陣列方式(但不是陣列)接受多個引數時很有用。

感謝 apply(),我們可以使用 Math.max()(請參閱其他函式)來判斷陣列中的最大元素

> Math.max(17, 33, 2)
33
> Math.max.apply(null, [17, 33, 2])
33

func.bind(thisValue, arg1, ..., argN)

這會執行部分函式應用,會建立一個新的函式,呼叫 func,其中 this 設定為 thisValue,後續引數:首先是 arg1,直到 argN,然後是新函式的實際引數。在下列非物件導向的設定中不需要 thisValue,因此它是 null

在此,我們使用 bind() 建立一個新的函式 plus1(),它類似於 add(),但只需要參數 y,因為 x 永遠是 1

function add(x, y) {
    return x + y;
}
var plus1 = add.bind(null, 1);
console.log(plus1(5));  // 6

換句話說,我們建立了一個等同於以下程式碼的新函式

function plus1(y) {
    return add(1, y);
}

處理遺失或額外的參數

JavaScript 不強制函式的元數:您可以使用任意數量的實際參數呼叫它,而與已定義的形式參數無關。因此,實際參數和形式參數的數量可能以兩種方式不同:

實際參數多於形式參數
額外的參數會被忽略,但可以使用特殊陣列類似變數 arguments(稍後討論)擷取。
實際參數少於形式參數
遺失的形式參數都具有值 undefined

所有參數依索引:特殊變數 arguments

特殊變數 arguments 僅存在於函式(包括方法)中。它是一個陣列類似物件,包含目前函式呼叫的所有實際參數。以下程式碼使用它:

function logArgs() {
    for (var i=0; i<arguments.length; i++) {
        console.log(i+'. '+arguments[i]);
    }
}

以下是互動

> logArgs('hello', 'world')
0. hello
1. world

arguments 具有以下特徵

參數的已棄用功能

嚴格模式會捨棄 幾個 arguments 比較不常見的功能:

  • arguments.callee 參照目前的函式。它主要用於在匿名函式中進行自我遞迴,且在嚴格模式中不被允許。作為解決方法,請使用命名函式表達式(請參閱 命名函式表達式),它可以透過其名稱參照自身。
  • 在非嚴格模式中,如果你變更參數,arguments 會保持最新狀態

    function sloppyFunc(param) {
        param = 'changed';
        return arguments[0];
    }
    console.log(sloppyFunc('value'));  // changed

    但在嚴格模式中不會進行此類更新

    function strictFunc(param) {
        'use strict';
        param = 'changed';
        return arguments[0];
    }
    console.log(strictFunc('value'));  // value
  • 嚴格模式禁止指派給變數 arguments(例如,透過 arguments++)。仍然允許指派給元素和屬性。

強制執行最小參數個數的強制參數

有三個 方法可以找出參數是否遺失。首先,你可以檢查它是否為 undefined

function foo(mandatory, optional) {
    if (mandatory === undefined) {
        throw new Error('Missing parameter: mandatory');
    }
}

其次,你可以將參數解釋為布林值。然後 undefined 會被視為 false。然而,有一個警告:其他幾個值也會被視為 false(請參閱 真值和假值),因此檢查無法區分,例如,0 和遺失的參數

if (!mandatory) {
    throw new Error('Missing parameter: mandatory');
}

第三,你也可以檢查 arguments 的長度,以強制執行最小參數個數:

if (arguments.length < 1) {
    throw new Error('You need to provide at least 1 argument');
}

最後一種方法與其他方法不同

  • 前兩種方法不會區分 foo()foo(undefined)。在兩種情況下,都會擲回例外。
  • 第三種方法會為 foo() 擲回例外,並將 optional 設定為 undefined 以供 foo(undefined) 使用。

可選參數

如果一個參數是可選的,表示如果它不存在,你會給它一個預設值。類似於強制參數,有四種選擇。

首先,檢查 undefined

function bar(arg1, arg2, optional) {
    if (optional === undefined) {
        optional = 'default value';
    }
}

其次,將 optional 解釋為布林值

if (!optional) {
    optional = 'default value';
}

第三,你可以使用或運算子 ||(請參閱 邏輯或 (||)),如果它不是假值,它會傳回左運算元。否則,它會傳回右運算元

// Or operator: use left operand if it isn't falsy
optional = optional || 'default value';

第四,你可以透過 arguments.length 檢查函式的項數

if (arguments.length < 3) {
    optional = 'default value';
}

再次強調,最後一種方法與其他方法不同

  • 前三種方法不會區分 bar(1, 2)bar(1, 2, undefined)。在兩種情況下,optional 都是 '預設值'
  • 第四種方法會將 bar(1, 2)optional 設為 '預設值',而將 bar(1, 2, undefined)optional 保留為 undefined(即不變更)。

另一個可能性是將可選參數作為 命名參數 傳入,例如物件文字的屬性(請參閱 命名參數)。

模擬傳遞參考參數

在 JavaScript 中,你無法傳遞參考參數;也就是說,如果你將變數傳遞給函式,它的值會被複製並傳遞給函式(傳遞值)。因此,函式無法變更變數。如果你需要這麼做,你必須包裝變數的值(例如,放入陣列中)。

這個範例示範一個會遞增變數的函式

function incRef(numberRef) {
    numberRef[0]++;
}
var n = [7];
incRef(n);
console.log(n[0]);  // 8

陷阱:意外的可選參數

如果你將函式 c 作為參數傳遞給另一個函式 f,那麼你必須注意兩個簽章:

  • f 預期其參數具有的簽章。 f 可能提供多個參數,而 c 可以決定使用其中幾個(如果有)。
  • c 的實際簽章。例如,它可能支援可選參數。

如果兩者不同,那麼你可能會得到意外的結果:c 可能有你不知道的可選參數,而且會錯誤地解釋 f 提供的額外引數。

舉例來說,考慮陣列方法 map()(請參閱 轉換方法),其參數通常是具有單一參數的函式

> [ 1, 2, 3 ].map(function (x) { return x * x })
[ 1, 4, 9 ]

你可以傳遞的一個函式是 parseInt()(請參閱 透過 parseInt() 取得整數

> parseInt('1024')
1024

你可能會(錯誤地)認為 map() 只提供單一引數,而 parseInt() 只接受單一引數。那麼你會對以下結果感到驚訝:

> [ '1', '2', '3' ].map(parseInt)
[ 1, NaN, NaN ]

map() 預期函式具有下列簽章

function (element, index, array)

parseInt() 具有下列簽章

parseInt(string, radix?)

因此,map() 不僅填入 string(透過 element),也填入 radix(透過 index)。這表示前一個陣列的值會以下列方式產生

> parseInt('1', 0)
1
> parseInt('2', 1)
NaN
> parseInt('3', 2)
NaN

總之,小心使用不確定的簽章函式和方法。如果您使用它們,通常會明確說明收到哪些參數以及傳遞哪些參數。這是透過回呼來達成

> ['1', '2', '3'].map(function (x) { return parseInt(x, 10) })
[ 1, 2, 3 ]

命名參數

在程式語言中呼叫 函式(或方法)時,您必須將實際參數(由呼叫者指定)對應到形式參數(函式定義的參數)。有兩種常見的方式可以做到這一點:

命名參數有兩個主要好處:它們會提供函式呼叫中引數的描述,而且它們適用於選用參數。我將先說明好處,然後向您展示如何透過物件文字在 JavaScript 中模擬命名參數。

命名參數作為描述

函式 只要有多於一個參數,您可能會對每個參數的用途感到困惑。例如,假設您有一個函式 selectEntries(),它會從資料庫傳回條目。針對下列函式呼叫:

selectEntries(3, 20, 2);

這三個數字代表什麼意思?Python 支援命名參數,而且它們可以輕鬆找出發生了什麼事

selectEntries(start=3, end=20, step=2)  # Python syntax

在 JavaScript 中模擬命名參數

JavaScript 不支援像 Python 和許多其他語言那樣的名稱參數。但有一個相當優雅的模擬:透過物件文字傳遞名稱參數,作為單一實際參數。當您使用此技術時,selectEntries() 的呼叫看起來像:

selectEntries({ start: 3, end: 20, step: 2 });

函式會接收一個具有 startendstep 屬性的物件。您可以省略其中任何一個

selectEntries({ step: 2 });
selectEntries({ end: 20, start: 3 });
selectEntries();

您可以實作 selectEntries() 如下

function selectEntries(options) {
    options = options || {};
    var start = options.start || 0;
    var end = options.end || getDbLength();
    var step = options.step || 1;
    ...
}

您也可以將位置參數與名稱參數結合使用。慣例上,後者會放在最後:

someFunc(posArg1, posArg2, { namedArg1: 7, namedArg2: true });

注意

在 JavaScript 中,這裡顯示的名稱參數模式有時稱為 選項選項物件(例如,由 jQuery 文件說明)。

下一步:16. 變數:範圍、環境和閉包