給急躁的程式設計師的 JavaScript(ES2022 版本)
請支持這本書:購買捐款
(廣告,請不要阻擋。)

25 可呼叫值



在本章中,我們將探討可調用的 JavaScript 值:函式、方法和類別。

25.1 函式的種類

JavaScript 有兩種類別的函式

請繼續閱讀以了解所有這些東西的含義。

25.2 一般函式

以下程式碼顯示了兩種執行(大致)相同事情的方法:建立一般函式。

// Function declaration (a statement)
function ordinary1(a, b, c) {
  // ···
}

// const plus anonymous (nameless) function expression
const ordinary2 = function (a, b, c) {
  // ···
};

在範圍內,函式宣告會提早啟動(請參閱 §11.8 “宣告:範圍和啟動”),並可以在宣告之前呼叫。這偶爾很有用。

變數宣告,例如 ordinary2 的宣告,不會提早啟動。

25.2.1 命名函式運算式(進階)

到目前為止,我們只看過匿名函式運算式,它們沒有名稱

const anonFuncExpr = function (a, b, c) {
  // ···
};

但也有命名函式運算式

const namedFuncExpr = function myName(a, b, c) {
  // `myName` is only accessible in here
};

myName 只能在函式主體內存取。函式可以使用它來參照自身(用於自我遞迴等),而與它被指定給哪個變數無關

const func = function funcExpr() { return funcExpr };
assert.equal(func(), func);

// The name `funcExpr` only exists inside the function body:
assert.throws(() => funcExpr(), ReferenceError);

即使沒有指定給變數,命名函式運算式也有名稱(第 A 行)

function getNameOfCallback(callback) {
  return callback.name;
}

assert.equal(
  getNameOfCallback(function () {}), ''); // anonymous

assert.equal(
  getNameOfCallback(function named() {}), 'named'); // (A)

請注意,透過函式宣告或變數宣告建立的函式始終都有名稱

function funcDecl() {}
assert.equal(
  getNameOfCallback(funcDecl), 'funcDecl');

const funcExpr = function () {};
assert.equal(
  getNameOfCallback(funcExpr), 'funcExpr');

函式有名稱的一個好處是這些名稱會顯示在 錯誤堆疊追蹤 中。

25.2.2 術語:函式定義和函式運算式

函式定義 是建立函式的語法

函式宣告總是產生一般函式。函式運算式產生一般函式或特殊函式

儘管函數宣告在 JavaScript 中仍然很受歡迎,但在現代程式碼中函數表達式幾乎都是箭頭函數。

25.2.3 函數宣告的部分

讓我們透過以下範例來檢視函數宣告的部分。大多數術語也適用於函數表達式。

function add(x, y) {
  return x + y;
}
25.2.3.1 參數清單中的尾隨逗號

JavaScript 一直允許並忽略陣列文字中的尾隨逗號。自 ES5 以來,它們也允許在物件文字中使用。自 ES2017 以來,我們可以將尾隨逗號新增到參數清單(宣告和呼叫)中

// Declaration
function retrieveData(
  contentText,
  keyword,
  {unique, ignoreCase, pageSize}, // trailing comma
) {
  // ···
}

// Invocation
retrieveData(
  '',
  null,
  {ignoreCase: true, pageSize: 10}, // trailing comma
);

25.2.4 一般函數扮演的角色

考慮前一節中的以下函數宣告

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

此函數宣告會建立一個一般函數,其名稱為 add。作為一般函數,add() 可以扮演三個角色

25.2.5 術語:實體與語法與角色(進階)

語法實體角色這幾個概念之間的區別很微妙,而且通常並不重要。但我希望讓您更了解它們

許多其他程式語言只有一個實體扮演實質函式的角色。然後,它們可以使用名稱函式來表示角色和實體。

25.3 特殊函式

特殊函式是一般函式的單一用途版本。它們各自專精於單一角色

除了語法更佳之外,每種特殊函式也支援新功能,讓它們比一般函式更擅長自己的工作。

表 16列出一般函式和特殊函式的功能。

表 16:四種函式的功能。如果儲存格值在括號中,表示某種限制。特殊變數this§25.3.3 “方法、一般函式和箭頭函式中的特殊變數this中說明。
函式呼叫 方法呼叫 建構函式呼叫
一般函式 (this === undefined)
箭頭函式 (字彙this)
方法 (this === undefined)
類別

25.3.1 特殊函式仍然是函式

請務必注意,箭頭函式、方法和類別仍然歸類為函式

> (() => {}) instanceof Function
true
> ({ method() {} }.method) instanceof Function
true
> (class SomeClass {}) instanceof Function
true

25.3.2 箭頭函式

將箭頭函式新增到 JavaScript 的原因有兩個

  1. 提供更簡潔的方式來建立函式。
  2. 它們在方法中作為實質函式時效果更好:方法可以透過特殊變數this來參照收到方法呼叫的物件。箭頭函式可以存取周圍方法的this,一般函式不能(因為它們有自己的this)。

我們將先探討箭頭函式的語法,然後探討this在各種函式中的運作方式。

25.3.2.1 箭頭函式的語法

讓我們回顧匿名函式表達式的語法

const f = function (x, y, z) { return 123 };

(大致)等價的箭頭函式如下所示。箭頭函式是表達式。

const f = (x, y, z) => { return 123 };

這裡,箭頭函式的函式主體是一個區塊。但它也可以是一個表達式。下列箭頭函式的工作方式與前一個完全相同。

const f = (x, y, z) => 123;

如果箭頭函式只有一個參數,而且該參數是一個識別碼(不是 解構模式),則可以省略參數周圍的括號

const id = x => x;

在將箭頭函式作為參數傳遞給其他函式或方法時,這很方便

> [1,2,3].map(x => x+1)
[ 2, 3, 4 ]

這個先前的範例展示了箭頭函式的一個好處 - 簡潔。如果我們使用函式表達式執行相同的任務,我們的程式碼會更冗長

[1,2,3].map(function (x) { return x+1 });
25.3.2.2 語法陷阱:從箭頭函式傳回物件文字

如果你希望箭頭函式的表達式主體是一個物件文字,你必須將文字放在括號中

const func1 = () => ({a: 1});
assert.deepEqual(func1(), { a: 1 });

如果你不這麼做,JavaScript 會認為箭頭函式有一個區塊主體(不會傳回任何東西)

const func2 = () => {a: 1};
assert.deepEqual(func2(), undefined);

{a: 1} 被解釋為一個區塊,其中包含 標籤 a: 和表達式陳述式 1。在沒有明確的 return 陳述式的情況下,區塊主體會傳回 undefined

這個陷阱是由於 語法歧義 造成的:物件文字和程式碼區塊具有相同的語法。我們使用括號告訴 JavaScript 主體是一個表達式(一個物件文字),而不是一個陳述式(一個區塊)。

25.3.3 方法、一般函式和箭頭函式中的特殊變數 this

  特殊變數 this 是物件導向功能

我們在此快速瀏覽特殊變數 this,以了解為什麼箭頭函式比一般函式更好的真正函式。

但這個功能僅在物件導向程式設計中很重要,並在 §28.5「方法和特殊變數 this 中有更深入的介紹。因此,如果你還不完全理解它,請不要擔心。

在方法內,特殊變數 this 讓我們可以存取接收者 - 收到方法呼叫的物件

const obj = {
  myMethod() {
    assert.equal(this, obj);
  }
};
obj.myMethod();

一般函式可以是方法,因此也具有隱含參數 this

const obj = {
  myMethod: function () {
    assert.equal(this, obj);
  }
};
obj.myMethod();

當我們將一般函式作為實質函式使用時,this 甚至是一個隱含參數。然後,它的值為 undefined(如果 嚴格模式 處於啟用狀態,這幾乎總是如此)

function ordinaryFunc() {
  assert.equal(this, undefined);
}
ordinaryFunc();

這表示用作實質函式的普通函式無法存取周圍方法的 this(A 行)。相反,箭頭函式不將 this 作為隱含參數。它們將其視為任何其他變數,因此可以存取周圍方法的 this(B 行)

const jill = {
  name: 'Jill',
  someMethod() {
    function ordinaryFunc() {
      assert.throws(
        () => this.name, // (A)
        /^TypeError: Cannot read properties of undefined \(reading 'name'\)$/);
    }
    ordinaryFunc();

    const arrowFunc = () => {
      assert.equal(this.name, 'Jill'); // (B)
    };
    arrowFunc();
  },
};
jill.someMethod();

在此程式碼中,我們可以觀察到處理 this 的兩種方式

25.3.4 建議:優先使用特殊函式而非一般函式

通常,你應該優先使用特殊函式而非一般函式,特別是類別和方法。

不過,當涉及到實質函式時,箭頭函式和一般函式之間的選擇就不那麼明確了

25.4 摘要:可呼叫值的種類

  此節參考後續內容

本節主要用作對目前和後續章節的參考。如果你不了解所有內容,請不用擔心。

到目前為止,我們所見的所有(實質)函式和方法都是

後面的章節會介紹其他程式設計模式

這些模式可以結合使用,例如,有同步可迭代物件和非同步可迭代物件。

有幾種新的函式和方法有助於處理某些模式組合

這讓我們有 4 種(2 × 2)函式和方法

表格 17 提供建立這 4 種函式和方法的語法概觀。

表格 17:建立函式和方法的語法。最後一欄指定實體產生的值數量。
結果 #
同步函式 同步方法
function f() {} { m() {} } 1
f = function () {}
f = () => {}
同步產生器函式 同步產生器方法
function* f() {} { * m() {} } 可迭代物件 0+
f = function* () {}
非同步函式 非同步方法
async function f() {} { async m() {} } Promise 1
f = async function () {}
f = async () => {}
非同步產生器函式 非同步產生器方法
async function* f() {} { async * m() {} } 非同步可迭代物件 0+
f = async function* () {}

25.5 傳回函式和方法的值

(本節中提到的所有內容都適用於函數和方法。)

return 陳述式明確地從函數回傳一個值

function func() {
  return 123;
}
assert.equal(func(), 123);

另一個範例

function boolToYesNo(bool) {
  if (bool) {
    return 'Yes';
  } else {
    return 'No';
  }
}
assert.equal(boolToYesNo(true), 'Yes');
assert.equal(boolToYesNo(false), 'No');

如果在函數的結尾處沒有明確回傳任何內容,JavaScript 會自動回傳 undefined

function noReturn() {
  // No explicit return
}
assert.equal(noReturn(), undefined);

25.6 參數處理

再次強調,本節中我只提及函數,但所有內容也適用於方法。

25.6.1 術語:參數與引數

參數引數 這兩個術語基本上是同義詞。如果你願意,可以做以下區分

25.6.2 術語:回呼

回呼回呼函數 是函數或方法呼叫的引數的函數。

以下是回呼的範例

const myArray = ['a', 'b'];
const callback = (x) => console.log(x);
myArray.forEach(callback);

// Output:
// 'a'
// 'b'

25.6.3 過多或過少的引數

如果函數呼叫提供的引數數量與函數定義預期的不同,JavaScript 也不會抱怨

例如

function foo(x, y) {
  return [x, y];
}

// Too many arguments:
assert.deepEqual(foo('a', 'b', 'c'), ['a', 'b']);

// The expected number of arguments:
assert.deepEqual(foo('a', 'b'), ['a', 'b']);

// Not enough arguments:
assert.deepEqual(foo('a'), ['a', undefined]);

25.6.4 參數預設值

參數預設值指定在未提供參數時要使用的值,例如

function f(x, y=0) {
  return [x, y];
}

assert.deepEqual(f(1), [1, 0]);
assert.deepEqual(f(), [undefined, 0]);

undefined 也會觸發預設值

assert.deepEqual(
  f(undefined, undefined),
  [undefined, 0]);

25.6.5 餘數參數

餘數參數是透過在識別碼之前加上三個點(...)來宣告。在函數或方法呼叫期間,它會收到一個包含所有剩餘引數的陣列。如果最後沒有多餘的引數,它會是一個空的陣列,例如

function f(x, ...y) {
  return [x, y];
}
assert.deepEqual(
  f('a', 'b', 'c'), ['a', ['b', 'c']]
);
assert.deepEqual(
  f(), [undefined, []]
);

與我們如何使用餘數參數相關的兩個限制

25.6.5.1 透過餘數參數強制執行一定的引數數量

你可以使用餘數參數來強制執行一定的引數數量。例如,以下函數

function createPoint(x, y) {
  return {x, y};
    // same as {x: x, y: y}
}

這是我們強制呼叫者永遠提供兩個引數的方式

function createPoint(...args) {
  if (args.length !== 2) {
    throw new Error('Please provide exactly 2 arguments!');
  }
  const [x, y] = args; // (A)
  return {x, y};
}

在 A 行中,我們透過解構存取 args 的元素。

25.6.6 命名參數

當有人呼叫函數時,呼叫者提供的引數會指定給被呼叫者接收的參數。執行對應的兩個常見方式是

  1. 位置參數:如果參數和實例具有相同位置,則實例會指定給參數。僅使用位置參數的函式呼叫如下所示。

    selectEntries(3, 20, 2)
  2. 命名參數:如果參數和實例具有相同名稱,則實例會指定給參數。JavaScript 沒有命名參數,但您可以模擬它們。例如,這是僅使用(模擬)命名參數的函式呼叫

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

命名參數有幾個好處

25.6.7 模擬命名參數

JavaScript 沒有真正的命名參數。模擬它們的官方方式是透過物件文字

function selectEntries({start=0, end=-1, step=1}) {
  return {start, end, step};
}

此函式使用解構存取其單一參數的屬性。它使用的模式是下列模式的縮寫

{start: start=0, end: end=-1, step: step=1}

此解構模式適用於空的物件文字

> selectEntries({})
{ start: 0, end: -1, step: 1 }

但如果您在沒有任何參數的情況下呼叫函式,它就不會運作

> selectEntries()
TypeError: Cannot read properties of undefined (reading 'start')

您可以透過為整個模式提供預設值來修正此問題。此預設值與較簡單參數定義的預設值相同:如果參數遺失,則使用預設值。

function selectEntries({start=0, end=-1, step=1} = {}) {
  return {start, end, step};
}
assert.deepEqual(
  selectEntries(),
  { start: 0, end: -1, step: 1 });

25.6.8 散佈(...)到函式呼叫中

如果您在函式呼叫的參數前面加上三個點(...),則表示您散佈它。這表示參數必須是可迭代物件,而迭代值全部都變成參數。換句話說,單一參數會擴充為多個參數,例如

function func(x, y) {
  console.log(x);
  console.log(y);
}
const someIterable = ['a', 'b'];
func(...someIterable);
  // same as func('a', 'b')

// Output:
// 'a'
// 'b'

散佈和 rest 參數使用相同的語法(...),但它們有相反的目的

25.6.8.1 範例:將參數 spread 到 Math.max()

Math.max() 會傳回其零個或多個參數中最大的那個。唉,它無法用於陣列,但 spread 提供了方法讓我們解決這個問題

> Math.max(-1, 5, 11, 3)
11
> Math.max(...[-1, 5, 11, 3])
11
> Math.max(-1, ...[-5, 11], 3)
11
25.6.8.2 範例:將參數 spread 到 Array.prototype.push()

類似地,陣列方法 .push() 會將其零個或多個參數破壞性地新增到陣列的尾端。JavaScript 沒有方法可以將陣列破壞性地附加到另一個陣列。再一次,spread 拯救了我們

const arr1 = ['a', 'b'];
const arr2 = ['c', 'd'];

arr1.push(...arr2);
assert.deepEqual(arr1, ['a', 'b', 'c', 'd']);

  練習:參數處理

25.7 函式的函式:.call().apply().bind()

函式是物件,而且有函式。在本節中,我們會探討其中三個函式:.call().apply().bind()

25.7.1 函式函式 .call()

每個函式 someFunc 都有下列函式

someFunc.call(thisValue, arg1, arg2, arg3);

這個函式呼叫大致等於下列函式呼叫

someFunc(arg1, arg2, arg3);

不過,使用 .call() 時,我們也可以指定 隱式參數 this 的值。換句話說:.call() 會讓隱式參數 this 變成明確的。

下列程式碼示範如何使用 .call()

function func(x, y) {
  return [this, x, y];
}

assert.deepEqual(
  func.call('hello', 'a', 'b'),
  ['hello', 'a', 'b']);

正如我們之前所見,如果我們函式呼叫一般函式,其 this 會是 undefined

assert.deepEqual(
  func('a', 'b'),
  [undefined, 'a', 'b']);

因此,前一個函式呼叫等於

assert.deepEqual(
  func.call(undefined, 'a', 'b'),
  [undefined, 'a', 'b']);

在箭頭函式中,透過 .call()(或其他方法)提供的 this 值會被忽略。

25.7.2 函式函式 .apply()

每個函式 someFunc 都有下列函式

someFunc.apply(thisValue, [arg1, arg2, arg3]);

這個函式呼叫大致等於下列函式呼叫(使用 spread

someFunc(...[arg1, arg2, arg3]);

不過,使用 .apply() 時,我們也可以指定 隱式參數 this 的值。

下列程式碼示範如何使用 .apply()

function func(x, y) {
  return [this, x, y];
}

const args = ['a', 'b'];
assert.deepEqual(
  func.apply('hello', args),
  ['hello', 'a', 'b']);

25.7.3 函式函式 .bind()

.bind() 是函式物件的另一個函式。這個函式會以下列方式呼叫

const boundFunc = someFunc.bind(thisValue, arg1, arg2);

.bind() 會傳回一個新的函式 boundFunc()。呼叫該函式會呼叫 someFunc(),其中 this 設定為 thisValue,且參數為:arg1arg2,接著是 boundFunc() 的參數。

也就是說,以下兩個函式呼叫是等效的

boundFunc('a', 'b')
someFunc.call(thisValue, arg1, arg2, 'a', 'b')
25.7.3.1 .bind() 的替代方案

預先填入 this 和參數的另一種方式是透過箭頭函式

const boundFunc2 = (...args) =>
  someFunc.call(thisValue, arg1, arg2, ...args);
25.7.3.2 .bind() 的實作

考量前一節,.bind() 可以實作為一個真實函式,如下所示

function bind(func, thisValue, ...boundArgs) {
  return (...args) =>
    func.call(thisValue, ...boundArgs, ...args);
}
25.7.3.3 範例:繫結一個真實函式

.bind() 用於真實函式有點不直觀,因為我們必須提供一個值給 this。由於在函式呼叫期間 thisundefined,因此通常會設定為 undefinednull

在以下範例中,我們建立 add8(),一個有一個參數的函式,方法是將 add() 的第一個參數繫結到 8

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

const add8 = add.bind(undefined, 8);
assert.equal(add8(1), 9);

  測驗

請參閱 測驗應用程式