函式作為一級物件(First‑class Function)
簡介
在 JavaScript 中,函式不只是執行程式碼的工具,它本身也是一個可以被操作、傳遞、存取的「值」── 也就是 first‑class(一級)物件。這個特性讓我們可以把函式當作參數傳入、從函式中回傳,甚至把它們存到陣列或物件裡,從而寫出更彈性、可組合且易於重用的程式碼。
對於剛踏入 JavaScript 的學習者來說,理解「函式是一級物件」的概念是邁向函式式程式設計(functional programming)的第一步;對於已有一定基礎的開發者而言,則是建構事件驅動、非同步流程、或是高階抽象(如 middleware、惰性計算)不可或缺的基礎。
本篇文章將從概念說明、實作範例、常見陷阱與最佳實踐,逐層剖析函式作為一級物件的威力,並提供實務上常見的應用情境,幫助你在日常開發中活用這項特性。
核心概念
1. 函式即值(Function as Value)
在 JavaScript 裡,函式可以賦值給變數、常數或屬性,與字串、數字、物件等其他資料型別沒有本質差別。
// 宣告一個函式,並指派給 const
const greet = function(name) {
return `哈囉,${name}!`;
};
// 直接呼叫
console.log(greet('小明')); // => 哈囉,小明!
重點:使用
const讓函式的參考不會被意外改寫,這是現代 JavaScript 的最佳實踐。
2. 把函式當作參數(Higher‑Order Function)
高階函式(Higher‑order function)是指 接受函式作為參數 或 回傳函式 的函式。這是函式作為一級物件最常見的應用。
// 一個簡易的陣列過濾器
function filter(array, predicate) {
const result = [];
for (const item of array) {
if (predicate(item)) { // predicate 本身是一個函式
result.push(item);
}
}
return result;
}
// 使用範例:找出所有偶數
const numbers = [1, 2, 3, 4, 5, 6];
const evenNumbers = filter(numbers, n => n % 2 === 0);
console.log(evenNumbers); // => [2, 4, 6]
技巧:使用箭頭函式 (
=>) 可以讓回呼函式的寫法更簡潔。
3. 從函式回傳函式(Function Returning Function)
回傳函式讓我們可以產生自訂化的行為,常見於工廠函式(factory)或柯里化(currying)。
// 工廠函式:產生帶有預設前綴的 logger
function createLogger(prefix) {
return function(message) {
console.log(`[${prefix}] ${message}`);
};
}
const errorLog = createLogger('ERROR');
errorLog('發生未預期的錯誤'); // => [ERROR] 發生未預期的錯誤
4. 函式可以存放於資料結構
因為函式是值,我們可以把它放進陣列、物件,甚至 Map、Set 裡,進一步實現指令表或策略模式。
const operations = {
add: (a, b) => a + b,
sub: (a, b) => a - b,
mul: (a, b) => a * b,
div: (a, b) => a / b,
};
function calculate(op, a, b) {
const fn = operations[op];
if (typeof fn !== 'function') throw new Error('未知的運算子');
return fn(a, b);
}
console.log(calculate('mul', 3, 4)); // => 12
5. call、apply、bind:改變 this 的函式
call、apply、bind 本身也是接受函式作為參數的高階函式,讓我們在執行時指定 this 指向。
function showInfo() {
console.log(`${this.name} 年齡 ${this.age}`);
}
const person = { name: '阿美', age: 28 };
showInfo.call(person); // => 阿美 年齡 28
// bind 會回傳一個新函式
const boundShowInfo = showInfo.bind(person);
boundShowInfo(); // => 阿美 年齡 28
程式碼範例(實用 5 篇)
| 範例編號 | 主題 | 說明 |
|---|---|---|
| 1 | 陣列的 map、filter、reduce |
以高階函式取代手寫迴圈 |
| 2 | 事件委派 | 把同類事件的處理抽成一個函式 |
| 3 | 防抖(debounce) | 使用閉包回傳控制頻率的函式 |
| 4 | Promise 中的 then |
函式作為非同步回呼 |
| 5 | 中介層(middleware) | 以函式陣列串接處理流程 |
範例 1:map、filter、reduce 的背後
const fruits = ['apple', 'banana', 'cherry', 'date'];
// map:把每個字串轉成大寫
const upper = fruits.map(f => f.toUpperCase());
// filter:只保留字母長度 > 5 的水果
const long = upper.filter(f => f.length > 5);
// reduce:把結果合併成一句話
const sentence = long.reduce((acc, cur) => `${acc}, ${cur}`, '水果有');
console.log(sentence); // => 水果有, BANANA, CHERRY, DATE
這三個方法本質上都是接受函式作為參數的高階函式,讓程式碼更具可讀性與可組合性。
範例 2:事件委派(Event Delegation)
// 假設有大量的按鈕,全部在同一個父容器內
document.getElementById('list').addEventListener('click', function(event) {
// 只處理被點擊的 button
if (event.target.tagName !== 'BUTTON') return;
const btn = event.target;
console.log(`你點了 ${btn.dataset.id} 按鈕`);
});
將所有按鈕的點擊行為交給父層的單一回呼函式,減少記憶體消耗,且便於 動態增減 按鈕。
範例 3:防抖(Debounce)函式
function debounce(fn, delay) {
let timer;
return function(...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
}
// 使用:搜尋框輸入時只在使用者停止輸入 300ms 後才呼叫 API
const search = debounce(query => console.log('搜尋', query), 300);
document.getElementById('searchBox').addEventListener('input', e => {
search(e.target.value);
});
這裡 debounce 回傳一個新函式,利用閉包保存 timer,達成控制執行頻率的目的。
範例 4:Promise 的 then
function fetchUser(id) {
return fetch(`https://api.example.com/users/${id}`)
.then(res => res.json()); // 這裡的 then 接收一個函式
}
fetchUser(42)
.then(user => {
console.log('使用者資料', user);
return user.posts; // 仍回傳值,讓下一個 then 繼續接收
})
.then(posts => console.log('貼文列表', posts))
.catch(err => console.error('發生錯誤', err));
then、catch、finally 都是 接受函式作為參數 的高階函式,讓非同步流程以「串接」方式表達。
範例 5:中介層(Middleware)串接
// 一個簡易的 Express 風格中介層機制
function compose(middlewares) {
return function (ctx) {
let index = -1;
function dispatch(i) {
if (i <= index) return Promise.reject(new Error('next() 呼叫重複'));
index = i;
const fn = middlewares[i];
if (!fn) return Promise.resolve();
return Promise.resolve(fn(ctx, () => dispatch(i + 1)));
}
return dispatch(0);
};
}
// 範例中介層
const logger = async (ctx, next) => {
console.log('開始', ctx.path);
await next();
console.log('結束', ctx.path);
};
const auth = async (ctx, next) => {
if (!ctx.user) throw new Error('未授權');
await next();
};
const handler = async ctx => {
ctx.body = `你好,${ctx.user.name}`;
};
const app = compose([logger, auth, handler]);
app({ path: '/home', user: { name: '小王' } })
.then(() => console.log('回應完成'))
.catch(err => console.error(err.message));
這段程式展示 「函式陣列」作為流程控制,每個中介層都是接受 ctx 與 next 兩個參數的函式,最終由 compose 產生的函式執行整條管線。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方案 |
|---|---|---|
this 失效 |
在回呼函式中直接使用 function,this 會被預設為 undefined(strict mode)或全域物件。 |
使用 箭頭函式 或 bind 明確綁定 this。 |
| 不小心改寫全域變數 | 把函式直接指派給未宣告的變數會產生隱式全域,導致衝突。 | 使用 const/let,或在模組化環境中 export/import。 |
| 閉包導致記憶體泄漏 | 閉包持有外部變數的引用,若不適時釋放會佔用大量記憶體(例如在大量 setInterval 中)。 |
在不需要時 手動 clearInterval,或在函式內部 限制引用範圍。 |
| 回呼地獄(Callback Hell) | 多層嵌套的回呼使程式碼難以閱讀與除錯。 | 使用 Promise、async/await 或 函式合成(如 compose)抽象化流程。 |
| 過度使用匿名函式 | 大量匿名函式會讓 stack trace 看不清來源,除錯困難。 | 為重要回呼 命名函式,或將複雜邏輯抽成獨立函式。 |
最佳實踐清單
- 用
const定義函式:避免意外改寫。 - 盡量使用箭頭函式:簡化
this綁定、提升可讀性。 - 函式保持單一職責:讓高階函式的參數更易理解。
- 善用內建高階函式(
map、filter、reduce、some、every等),減少手寫迴圈。 - 在需要多次重複使用的回呼時,抽成可重用的函式或工廠(如 debounce、throttle)。
- 使用模組系統(ESM、CommonJS)封裝函式,避免全域汙染。
- 對於非同步流程,優先考慮
async/await,讓程式流程看起來像同步程式。
實際應用場景
| 場景 | 為何需要函式作為一級物件 | 範例說明 |
|---|---|---|
| UI 事件處理 | 多個元素共享同一套邏輯,且需要動態掛載/解除 | 事件委派、動態 addEventListener |
| 資料流管線(Data Pipelines) | 每一步都是純函式,前一步的輸出直接成為下一步的輸入 | Array.prototype.reduce、RxJS 流 |
| Middleware / Plugin 系統 | 允許外部開發者以函式形式掛載自訂行為 | Express、Koa、Redux 中間件 |
| 函式式程式設計(Functional Programming) | 高階函式、柯里化、偏函式等概念皆仰賴函式作為值 | Lodash/fp、Ramda |
| 非同步佇列(Task Queue) | 把任務封裝成函式,依序或平行執行 | Promise.all, async/await 佇列實作 |
| 測試與 Mock | 以函式取代真實實作,方便注入測試行為 | Jest 的 mock function、Sinon stub |
實務 tip:在設計 API 時,若你預期使用者會自行提供回呼(如
onSuccess、onError),務必在文件中說明 回呼函式的參數與this行為,避免使用者因this被意外綁定而產生 bug。
總結
JavaScript 的函式是 一級物件,這意味著它們可以像字串或數字一樣被儲存、傳遞、回傳。
透過 高階函式、閉包、函式陣列 等技巧,我們能夠寫出 可組合、易測試、且高度抽象 的程式碼。
在開發過程中,掌握以下要點即可事半功倍:
- 把函式當作值:用
const保存,必要時傳遞或回傳。 - 善用高階函式:
map、filter、reduce、自訂compose、pipe等。 - 注意
this與閉包:使用箭頭函式或bind,避免記憶體泄漏。 - 保持函式單一職責:讓組合更簡潔,除錯更容易。
- 在非同步與事件驅動情境,以函式作為回呼或中介層,提升彈性與可維護性。
只要熟悉並善用「函式作為一級物件」的特性,你的 JavaScript 程式碼將會變得更模組化、可讀且具備高度可重用性,為日後的專案擴充與維護奠定堅實基礎。祝你寫程式愉快! 🎉