本章探討函式呼叫的核心機制,包括兩個隱式參數 argumentsthis、四種函式呼叫方式,以及解決 function context 問題的技巧。

使用隱式函式參數#

函式被呼叫時,除了明確定義的參數外,還會隱式傳入兩個參數:argumentsthis。它們不會出現在函式簽名中,但可以在函式內部像一般參數一樣存取。

arguments 參數#

  • arguments 是一個包含所有傳入引數的類陣列物件(array-like),擁有 length 屬性,可透過索引存取個別引數
  • 它不是真正的 JavaScript 陣列,無法使用 sort 等陣列方法
  • 主要用途:存取所有傳入的引數,無論是否有對應的具名參數,可用來實作函式多載(function overloading)或接受任意數量引數的函式
function sum() {
  var sum = 0;
  for (var i = 0; i < arguments.length; i++) {
    sum += arguments[i];
  }
  return sum;
}
assert(sum(1, 2, 3) === 6, "We can add three numbers");

arguments 與參數的別名關係(aliasing):

  • 在非嚴格模式下,arguments 物件與具名參數互為別名 – 修改 arguments[0] 會同時改變對應的具名參數,反之亦然
  • strict mode 下,這種別名關係被禁用,arguments 與具名參數各自獨立

ES6 的 rest 參數是真正的陣列,在許多場景下可取代 arguments 物件。但理解 arguments 仍然重要,因為在維護舊程式碼時會經常遇到。

this 參數:函式上下文#

  • this 參數代表函式上下文(function context),指向與該次函式呼叫相關聯的物件
  • 在 Java 或 C# 中,this 由函式定義的位置決定;但在 JavaScript 中,this 的值很大程度取決於函式被呼叫的方式

this 不僅由函式定義的方式和位置決定,也會受到呼叫方式的強烈影響。這是物件導向 JavaScript 的核心概念之一。

函式呼叫的四種方式#

JavaScript 中函式可以透過四種方式呼叫,每種方式對 this 的影響各不相同:

作為函式呼叫(as a function)#

  • 使用 () 運算子直接呼叫,且表達式不是物件的屬性參照
  • 非嚴格模式this 指向全域物件(window
  • 嚴格模式thisundefined
function ninja() {
  return this;
}
function samurai() {
  "use strict";
  return this;
}

assert(ninja() === window); // 非嚴格模式
assert(samurai() === undefined); // 嚴格模式

作為方法呼叫(as a method)#

  • 函式被指定為物件的屬性,並透過該屬性呼叫時,this 指向該物件
  • 同一個函式可以被不同物件引用,this 會隨著呼叫的物件而改變
function whatsMyContext() {
  return this;
}

var ninja1 = { getMyThis: whatsMyContext };
var ninja2 = { getMyThis: whatsMyContext };

assert(ninja1.getMyThis() === ninja1); // this 是 ninja1
assert(ninja2.getMyThis() === ninja2); // this 是 ninja2

以方法呼叫函式是撰寫物件導向 JavaScript 的關鍵,讓你可以在方法內透過 this 引用該方法「所屬」的物件。

Figure 4.2: 當函式作為方法呼叫時,this 指向該方法所屬的物件

作為建構函式呼叫(as a constructor)#

使用 new 關鍵字呼叫函式時,會觸發以下特殊行為:

  1. 建立一個新的空物件
  2. 該物件作為 this 傳入建構函式(成為函式上下文)
  3. 新建立的物件作為 new 運算子的回傳值

建構函式的回傳值規則:

  • 若建構函式回傳一個物件,該物件會取代 new 產生的物件成為回傳值
  • 若回傳非物件值(如數字、字串),回傳值會被忽略,仍回傳新建立的物件
function Ninja() {
  this.skulk = function () {
    return this;
  };
}
var ninja1 = new Ninja();
assert(ninja1.skulk() === ninja1); // this 指向新建立的物件

建構函式通常以大寫字母開頭的名詞命名(如 NinjaEmperor),而一般函式和方法以小寫字母開頭的動詞命名(如 skulkcreep)。如果不小心以一般方式呼叫建構函式(忘了 new),在非嚴格模式下屬性會被加到 window 上,在嚴格模式下則會崩潰。

使用 apply 和 call 方法呼叫#

  • applycall 是所有函式都有的方法,讓你能明確指定任意物件作為函式上下文
  • apply(context, argsArray):第二個參數是引數陣列
  • call(context, arg1, arg2, ...):引數逐一列出
function juggle() {
  var result = 0;
  for (var n = 0; n < arguments.length; n++) {
    result += arguments[n];
  }
  this.result = result;
}

var ninja1 = {};
juggle.apply(ninja1, [1, 2, 3, 4]); // ninja1.result === 10
juggle.call(ninja1, 5, 6, 7, 8); // ninja1.result === 26

如何選擇 applycall

  • 引數已經在陣列中 -> 用 apply
  • 引數是獨立的值 -> 用 call
  • 兩者功能相同,差別僅在引數的傳遞方式

在 callback 中強制設定函式上下文的實用範例:自行實作 forEach 函式,使用 call 將每個迭代項目設為 callback 的函式上下文。

Figure 4.5: 使用 call 和 apply 可以手動指定函式的 this 上下文

修正函式上下文的問題#

在 callback 函式(如事件處理器)中,this 可能不如預期。例如按鈕的 click handler 中,this 會指向觸發事件的 HTML 元素,而非建立 handler 的物件。

使用箭頭函式(Arrow Functions)#

  • 箭頭函式沒有自己的 this,它會記住定義時所在環境的 this
  • 在建構函式內使用箭頭函式作為 callback,this 會正確指向新建立的物件
function Button() {
  this.clicked = false;
  this.click = () => {
    this.clicked = true;
    assert(button.clicked, "The button has been clicked");
  };
}

Figure 4.6: 箭頭函式沒有自己的 this 上下文,而是繼承定義時的外圍上下文

箭頭函式與物件字面值的陷阱: 若箭頭函式定義在全域程式碼的物件字面值中,this 會是全域的 window 物件,而非該物件字面值。因為箭頭函式在建立時擷取 this,而物件字面值不會產生新的函式上下文。

Figure 4.7: 在物件字面量中定義的箭頭函式,其 this 指向全域物件

使用 bind 方法#

  • bind 方法可為任何函式建立一個新函式,其 this 永遠綁定到指定的物件,不受呼叫方式影響
  • bind 不會修改原始函式,而是建立一個全新的函式
var boundFunction = button.click.bind(button);
elem.addEventListener("click", boundFunction);
// boundFunction 的 this 永遠是 button
assert(boundFunction !== button.click); // 是全新的函式

本章重點整理#

  • 函式呼叫時隱式傳入 arguments(所有引數的集合)和 this(函式上下文)
  • 函式有四種呼叫方式,每種對 this 的影響不同:
    • 作為函式:非嚴格模式下 thiswindow,嚴格模式下是 undefined
    • 作為方法this 是呼叫方法的物件
    • 作為建構函式this 是新建立的物件
    • 透過 call/applythis 是第一個引數指定的物件
  • 箭頭函式沒有自己的 this,繼承定義時的 this
  • bind 方法建立一個 this 永遠綁定到指定物件的新函式