本章涵蓋內容#

  • 為什麼理解函式如此關鍵
  • 函式如何作為 first-class objects
  • 定義函式的各種方式
  • 參數賦值的祕密

撰寫精緻的 JavaScript 程式碼,關鍵在於將 JavaScript 當作一門**函式式語言(functional language)**來學習。你所撰寫的所有程式碼的精密程度,都取決於這個認知。


3.1 What’s with the functional difference?#

函式和函式式概念在 JavaScript 中如此重要的原因之一,在於函式是主要的模組化執行單元(primary modular units of execution)。除了頁面建構階段執行的全域 JavaScript 程式碼外,我們撰寫的幾乎所有腳本程式碼都會在某個函式內。

在 JavaScript 中,物件(objects)擁有以下能力:

  • 透過字面值(literals)建立:{}
  • 賦值給變數、陣列元素、其他物件的屬性
  • 作為引數(arguments)傳遞給函式
  • 作為函式的回傳值
  • 擁有可動態建立和賦值的屬性

而在 JavaScript 中,函式也能做到以上所有事情

Functions as first-class objects#

JavaScript 中的函式擁有物件的所有能力,因此被視為與其他物件同等對待。我們稱函式為 first-class objects(一等物件),又常被稱為 first-class citizens(一等公民)。函式可以:

  • 透過字面值建立:function ninjaFunction() {}
  • 賦值給變數、陣列元素、其他物件的屬性:
var ninjaFunction = function () {};
ninjaArray.push(function () {});
ninja.data = function () {};
  • 作為引數傳遞給其他函式
  • 作為函式的回傳值
  • 擁有可動態建立和賦值的屬性:
var ninjaFunction = function () {};
ninjaFunction.name = "Hanzo";

函式與物件能做的事情相同,但函式還有一個額外的特殊能力:它們是可呼叫的(invocable)——函式可以被呼叫或調用以執行某個動作。

函式式程式設計(Functional Programming) 是一種以組合函式來解決問題的程式設計風格,與更主流的命令式程式設計不同。函式式程式設計可以幫助我們撰寫更容易測試、擴展和模組化的程式碼。

Callback functions#

First-class objects 的特性之一是可以作為引數傳遞給函式。當我們將一個函式作為引數傳遞給另一個函式,在稍後的某個時間點呼叫它,這就是 callback function(回呼函式) 的概念。

function useless(ninjaCallback) {
  return ninjaCallback();
}

這段程式碼展示了將函式作為引數傳遞給另一個函式,並透過傳入的參數呼叫該函式的能力。

回呼函式的實際應用場景包括:

  • 事件處理:將回呼函式設定為事件處理器,由瀏覽器在特定事件發生時呼叫
document.body.addEventListener("mousemove", function () {
  var second = document.getElementById("second");
  addMessage(second, "Event: mousemove");
});
  • 排序比較器:透過 sort 方法的回呼函式,自訂排序邏輯
var values = [0, 3, 2, 5, 7, 4, 8, 1];
values.sort(function (value1, value2) {
  return value1 - value2;
});

回呼函式不一定要是非同步的。上述範例中既有同步回呼(如 useless 函式範例),也有非同步回呼(如 mousemove 事件範例)。兩者都是回呼。

Figure 3.1: 執行 callback 範例的結果


3.2 Fun with functions as objects#

函式與其他物件型別共享許多相似之處。一個可能令人驚訝的能力是:我們可以為函式附加屬性(properties)

var wieldSword = function () {};
wieldSword.swordType = "katana";

利用這個能力,可以實現兩個有趣的應用:

  • Storing functions in a collection:輕鬆管理相關函式的集合
  • Memoization:讓函式記住先前計算過的值,提升後續呼叫的效能

Storing functions#

當我們需要管理一組回呼函式的集合(例如某個事件發生時需要呼叫的函式),我們會想儲存唯一的函式集合,避免重複。

書中的做法是利用函式的屬性來追蹤:

var store = {
  nextId: 1,
  cache: {},
  add: function (fn) {
    if (!fn.id) {
      fn.id = this.nextId++;
      this.cache[fn.id] = fn;
      return true;
    }
  },
};

add 方法中,首先檢查函式是否已有 id 屬性。若有,表示已被處理過,忽略它;若無,則賦予一個 id,存入 cache,並回傳 true 表示新增成功。

Figure 3.3: 在函式上附加屬性來追蹤函式

Self-memoizing functions#

Memoization(記憶化)是建構一個能記住先前計算值的函式的過程。每當函式計算出結果,就將該結果連同函式引數一起儲存。當下次以相同引數呼叫時,直接回傳先前儲存的結果,不必重新計算。

function isPrime(value) {
  if (!isPrime.answers) {
    isPrime.answers = {};
  }
  if (isPrime.answers[value] !== undefined) {
    return isPrime.answers[value];
  }
  var prime = value !== 1;
  for (var i = 2; i < value; i++) {
    if (value % i === 0) {
      prime = false;
      break;
    }
  }
  return (isPrime.answers[value] = prime);
}

快取是函式本身的屬性,因此只要函式存在,快取就存在。

優點

  • 使用者在呼叫先前已計算過的值時,可獲得效能提升
  • 一切在幕後無縫運作,使用者和開發者無需額外操作

缺點

  • 任何形式的快取都會犧牲記憶體換取效能
  • 純粹主義者可能認為快取不應混入商業邏輯(第 8 章會介紹如何解決)
  • 難以對此類演算法進行負載測試或效能量測,因為結果取決於先前的輸入

3.3 Defining functions#

JavaScript 函式通常透過**函式字面值(function literal)**來定義。JavaScript 提供了幾種定義函式的方式,可分為四組:

  • Function declarations(函式宣告)和 function expressions(函式表達式)——最常見的兩種方式
  • Arrow functions(箭頭函式)——ES6 新增,語法更精簡
  • Function constructors——較少使用,可從字串動態建構函式
  • Generator functions(生成器函式)——ES6 新增,可在執行過程中退出和重新進入

Function declarations and function expressions#

兩者是 JavaScript 中定義函式最常見的方式,非常相似但有微妙差異。

Function declarations(函式宣告)

  • function 關鍵字開頭
  • 函式名稱是必要的
  • 必須作為獨立的 JavaScript 陳述句(但可包含在其他函式內部)
function samurai() {
  return "samurai here";
}

function ninja() {
  function hiddenNinja() {
    return "ninja here";
  }
  return hiddenNinja();
}

Figure 3.4: 函式宣告作為獨立的 JavaScript 程式碼區塊

Function expressions(函式表達式)

  • 總是作為另一個陳述句的一部分(例如賦值的右側、函式呼叫的引數)
  • 函式名稱是可選的
var myFunc = function () {}; // 賦值給變數
myFunc(function () {
  return function () {};
}); // 作為引數與回傳值
(function namedFunctionExpression() {})(); // 具名函式表達式,立即呼叫

Immediately Invoked Function Expression (IIFE)

立即呼叫的函式表達式是 JavaScript 開發中的重要概念,可用於模擬模組。需要用括號包裹函式表達式,讓 JavaScript 解析器知道這是一個表達式而非陳述句:

(function () {})(); // 標準寫法
(function () {})(); // 另一種寫法
+(function () {})(); // 使用一元運算子的寫法

Figure 3.5: 標準函式呼叫與立即執行函式表達式的比較

Arrow functions#

Arrow functions(箭頭函式)是 ES6 新增的語法糖,讓我們能以更簡短的方式定義函式。

// 函式表達式
var values = [0, 3, 2, 5, 7, 4, 8, 1];
values.sort(function (value1, value2) {
  return value1 - value2;
});

// 箭頭函式——更精簡
values.sort((value1, value2) => value1 - value2);

箭頭函式的語法有兩種形式:

  • 簡短形式param => expression——回傳值就是該表達式的值
  • 區塊形式(param1, param2) => { statements }——回傳值的行為與標準函式相同
var greet = (name) => "Greetings " + name; // 簡短形式

var greet = (name) => {
  // 區塊形式
  var helloString = "Greetings ";
  return helloString + name;
};

當只有一個參數時,括號可以省略。零個或多個參數時,括號是必要的。

Figure 3.6: 箭頭函式的語法


3.4 Arguments and function parameters#

在討論函式時,parameterargument 這兩個術語常被混用,但它們有明確的區別:

  • Parameter(參數):在函式定義中列出的變數
  • Argument(引數):在呼叫函式時傳入的值

當引數列表與參數列表長度不同時:

  • 若引數多於參數,多餘的引數不會被賦值給任何參數名稱
  • 若引數少於參數,未被賦值的參數值為 undefined

Rest parameters#

Rest parameters(其餘參數)是 ES6 新增的功能,透過在最後一個具名參數前加上省略號 ...,將其轉換為一個陣列,包含所有剩餘的傳入引數。

function multiMax(first, ...remainingNumbers) {
  var sorted = remainingNumbers.sort(function (a, b) {
    return b - a;
  });
  return first * sorted[0];
}
assert(multiMax(3, 1, 2, 3) == 9, "3*3=9 (First arg, by largest.)");

只有最後一個函式參數才能是 rest parameter。若嘗試在非最後的參數前加上省略號,會產生 SyntaxError: parameter after rest parameter 錯誤。

Default parameters#

Default parameters(預設參數)也是 ES6 新增的功能。在 ES6 之前,開發者需要手動檢查參數是否為 undefined 來實現預設值:

// ES6 之前的做法
function performAction(ninja, action) {
  action = typeof action === "undefined" ? "skulking" : action;
  return ninja + " " + action;
}

ES6 的語法直接在參數列表中指定預設值:

// ES6 的做法
function performAction(ninja, action = "skulking") {
  return ninja + " " + action;
}

預設值可以是任何值:原始值、物件、陣列,甚至函式。預設值在每次函式呼叫時從左到右求值,後面的預設參數可以引用前面的參數:

function performAction(
  ninja,
  action = "skulking",
  message = ninja + " " + action
) {
  return message;
}
assert(performAction("Yoshi") === "Yoshi skulking");

適度使用預設參數——作為避免 null 值的手段,或作為設定函式行為的簡單旗標——可以讓程式碼更簡潔優雅。但過度使用可能降低可讀性。


3.5 Summary#

  • 撰寫精緻的程式碼取決於將 JavaScript 當作函式式語言來學習
  • 函式是 first-class objects,在 JavaScript 中與其他物件同等對待,可以透過字面值建立、賦值給變數、作為參數傳遞、作為回傳值、賦予屬性和方法
  • Callback functions 是其他程式碼稍後會「回呼」的函式,常見於事件處理
  • 我們可以利用函式的屬性來儲存資訊:
    • 在函式屬性中儲存函式集合,便於後續引用和呼叫
    • 利用函式屬性建立快取(memoization),避免不必要的重複計算
  • 函式的定義方式包括:function declarations、function expressions、arrow functions、function generators
  • Function declarations 必須有名稱,必須作為獨立陳述句;function expressions 不必有名稱,但必須是其他程式碼陳述句的一部分
  • Arrow functions 是 ES6 新增的語法,能以更精簡的方式定義函式
  • Parameter 是函式定義中的變數,argument 是呼叫函式時傳入的值;兩者的數量可以不同
  • Rest parametersdefault parameters 是 ES6 的新功能:
    • Rest parameters 讓我們可以引用沒有對應參數名稱的剩餘引數
    • Default parameters 讓我們可以指定在呼叫時未提供值時使用的預設參數值