本文 AI 產出,尚未審核

函式作為一級物件(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. 函式可以存放於資料結構

因為函式是值,我們可以把它放進陣列、物件,甚至 MapSet 裡,進一步實現指令表策略模式

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. callapplybind:改變 this 的函式

callapplybind 本身也是接受函式作為參數的高階函式,讓我們在執行時指定 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 陣列的 mapfilterreduce 以高階函式取代手寫迴圈
2 事件委派 把同類事件的處理抽成一個函式
3 防抖(debounce) 使用閉包回傳控制頻率的函式
4 Promise 中的 then 函式作為非同步回呼
5 中介層(middleware) 以函式陣列串接處理流程

範例 1:mapfilterreduce 的背後

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));

thencatchfinally 都是 接受函式作為參數 的高階函式,讓非同步流程以「串接」方式表達。


範例 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));

這段程式展示 「函式陣列」作為流程控制,每個中介層都是接受 ctxnext 兩個參數的函式,最終由 compose 產生的函式執行整條管線。


常見陷阱與最佳實踐

陷阱 說明 解決方案
this 失效 在回呼函式中直接使用 functionthis 會被預設為 undefined(strict mode)或全域物件。 使用 箭頭函式bind 明確綁定 this
不小心改寫全域變數 把函式直接指派給未宣告的變數會產生隱式全域,導致衝突。 使用 const/let,或在模組化環境中 export/import
閉包導致記憶體泄漏 閉包持有外部變數的引用,若不適時釋放會佔用大量記憶體(例如在大量 setInterval 中)。 在不需要時 手動 clearInterval,或在函式內部 限制引用範圍
回呼地獄(Callback Hell) 多層嵌套的回呼使程式碼難以閱讀與除錯。 使用 Promiseasync/await函式合成(如 compose)抽象化流程。
過度使用匿名函式 大量匿名函式會讓 stack trace 看不清來源,除錯困難。 為重要回呼 命名函式,或將複雜邏輯抽成獨立函式。

最佳實踐清單

  1. const 定義函式:避免意外改寫。
  2. 盡量使用箭頭函式:簡化 this 綁定、提升可讀性。
  3. 函式保持單一職責:讓高階函式的參數更易理解。
  4. 善用內建高階函式mapfilterreducesomeevery 等),減少手寫迴圈。
  5. 在需要多次重複使用的回呼時,抽成可重用的函式或工廠(如 debounce、throttle)。
  6. 使用模組系統(ESM、CommonJS)封裝函式,避免全域汙染。
  7. 對於非同步流程,優先考慮 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 時,若你預期使用者會自行提供回呼(如 onSuccessonError),務必在文件中說明 回呼函式的參數與 this 行為,避免使用者因 this 被意外綁定而產生 bug。


總結

JavaScript 的函式是 一級物件,這意味著它們可以像字串或數字一樣被儲存、傳遞、回傳。
透過 高階函式閉包函式陣列 等技巧,我們能夠寫出 可組合、易測試、且高度抽象 的程式碼。

在開發過程中,掌握以下要點即可事半功倍:

  1. 把函式當作值:用 const 保存,必要時傳遞或回傳。
  2. 善用高階函式mapfilterreduce、自訂 composepipe 等。
  3. 注意 this 與閉包:使用箭頭函式或 bind,避免記憶體泄漏。
  4. 保持函式單一職責:讓組合更簡潔,除錯更容易。
  5. 在非同步與事件驅動情境,以函式作為回呼或中介層,提升彈性與可維護性。

只要熟悉並善用「函式作為一級物件」的特性,你的 JavaScript 程式碼將會變得更模組化、可讀且具備高度可重用性,為日後的專案擴充與維護奠定堅實基礎。祝你寫程式愉快! 🎉