本章涵蓋內容#
- 為什麼理解函式如此關鍵
- 函式如何作為 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#
在討論函式時,parameter 和 argument 這兩個術語常被混用,但它們有明確的區別:
- 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 parameters 和 default parameters 是 ES6 的新功能:
- Rest parameters 讓我們可以引用沒有對應參數名稱的剩餘引數
- Default parameters 讓我們可以指定在呼叫時未提供值時使用的預設參數值