javaScript : 函數種類、宣告、參數 vs. 引數、返回值



寫 JavaScript 久了就會發現:程式可不可維護,常常決勝在「函數」這層。這篇不繞口令,直接把重點梳開:函數有哪些種類(宣告、表達式、箭頭、IIFE、方法、建構、生成器、async)、宣告語法差在哪、參數與引數怎麼分、返回值怎麼設計。

接著用實例說明預設值、剩餘參數、解構與型別檢查,補齊 this 與箭頭函數的常見坑,再把非同步(callback/Promise/async)和高階函數、閉包、節流防抖放進實務場景。

文末還有實作步驟與雷點清單,幫你少踩一堆坑,寫出可讀、可測、好擴充的函數。希望本篇文章可以幫助到需要的您。


目錄

{tocify} $title={目錄} 


函數是什麼?為什麼重要

函數(Function)是可重複呼叫的一段程式碼,用來包裝邏輯、接收參數(Parameter)並處理引數(Argument),最後通常會回傳返回值(Return)。在 JavaScript 裡,函數是一等公民(First-class citizen):可以被存到變數、當成參數傳進去、當成返回值丟出來,這也讓 JS 很適合做高階函數、函數式設計與事件導向的開發。


函數有哪些種類

先給你一張腦內地圖,後面每種都會講:

宣告方式層面

        函數宣告(Function Declaration)

        函數表達式(Function Expression)

        箭頭函數(Arrow Function)

        立即執行函數(IIFE, Immediately Invoked Function Expression)

語意與用途層面

        方法(Object Method) / 類別方法(Class Method)

        建構函數(Constructor Function,傳統 function + new)

        生成器(Generator Function,function*)

        非同步函數(async function)

        高階函數(Higher-Order Function,接或回傳函數)

        純函數 / 非純函數(是否有副作用)

        閉包(Closure)

ES6+ 與語法糖

        預設參數、剩餘參數(rest)、展開(spread)

        參數解構(object/array destructuring)


函數的宣告方式與語法差異

1.    函數宣告(Function Declaration)

function add(a, b) {
  return a + b;
}


特點:

可被 hoist(提升):整個宣告在所在作用域最上方可用。

適合放公共工具、無條件在檔內可用的函數。


2.    函數表達式(Function Expression)

const add = function (a, b) {
  return a + b;
};


特點:

只會 hoist 變數名稱,不會 hoist 初始化值;宣告前呼叫會噴錯。

常用於將函數存入變數、傳入其他函數。


3.    箭頭函數(Arrow Function)

const add = (a, b) => a + b;          // 簡寫:表達式直接回傳
const addBlock = (a, b) => { return a + b; }; // 區塊必須寫 return


特點(非常重要):

不綁定自己的 this、arguments、super、new.target。this 取決於外層作用域。

不能當建構子(不能 new)。

單行表達式自動回傳;但一旦加上 {} 區塊,就必須手動 return。


4.    立即執行函數(IIFE)

(function () {
  // 私有作用域
  const token = "secret";
  console.log("IIFE run once");
})();


用途:

建立獨立作用域,避免污染全域(在模組普及前很常見)。


參數(Parameter)與引數(Argument):形式參數 vs. 實際參數

參數(Parameter):函數宣告時列在括號中的名稱(又稱形式參數 / 形參)。

引數(Argument):呼叫函數時實際傳入的值(又稱實際參數 / 實參)。

function greet(name) {        // name 是參數(形參)
  console.log(`Hi, ${name}!`);
}
greet("Ada");                 // "Ada" 是引數(實參)


對應關係:呼叫時,實參會依位置(或命名解構)賦值給形參。


參數重點:預設值、剩餘參數、解構、型別與驗證

1.   預設參數(Default Parameters)

function drawChart(width = 800, height = 600) {
  return { width, height };
}


雷點:

參數的預設值只在參數是 undefined 時生效,傳 null 不會觸發預設。

物件或陣列當預設值是同一個實例,一般 OK,但若你要在函數內改那份物件,會共用狀態;建議用工廠函數回傳新物件:

function makeDefaultConfig() { return { theme: "light" }; }
function init(config = makeDefaultConfig()) { return config; }


2.   剩餘參數(Rest Parameter)

把不確定數量的實參收成陣列:

function sum(...nums) {
  return nums.reduce((acc, n) => acc + n, 0);
}
sum(1, 2, 3); // 6


3.   展開(Spread)搭配呼叫

const args = [1, 2, 3];
sum(...args); // 等同 sum(1, 2, 3)


4.   參數解構(Destructuring)

物件解構:

function createUser({ name, email, admin = false }) {
  if (!name || !email) throw new Error("name/email 必填");
  return { name, email, admin };
}
createUser({ name: "Joe", email: "j@x.io" });


陣列解構:

function firstTwo([a, b] = []) {
  return { a, b };
}
firstTwo([10, 20]); // { a: 10, b: 20 }


雷點:

解構參數時,務必給預設值避免 Cannot destructure property 'x' of undefined。

預設值順序重要,function f({a} = {}) {} 比 function f({a}) {} 安全。


5.   型別與驗證(在純 JS)

JS 沒原生型別註記(TypeScript 才有),建議自己做防禦性檢查:

function areaOfCircle(r) {
  if (typeof r !== "number" || Number.isNaN(r) || r < 0) {
    throw new TypeError("r 必須是非負數");
  }
  return Math.PI * r * r;
}


6.   傳值 / 傳參考的誤會

JS 只有「按值傳遞」。但物件的值是參考(reference),所以你把物件傳進去,函數內改動該物件的屬性,外面也會看到改變。

要避免副作用,可在函數內淺拷貝 / 深拷貝:

function renameUser(user, newName) {
  return { ...user, name: newName }; // 回傳新物件,避免改到原本的
}


返回值(Return Value)與控制流程

1.   基本

function add(a, b) {
  return a + b;  // return 後面的程式不會執行
}


2.   沒寫 return 會回傳 undefined

function log(v) { console.log(v); } // 回傳 undefined


3.   早退(Early Return)

function save(user) {
  if (!user) return false;    // 早退,讓分支扁平
  // ...其餘處理
  return true;
}


4.    ASI(自動分號插入)陷阱

function f() {
  return              // ← 這裡被 ASI 插入分號
  { ok: true }
}
f() // 其實回傳 undefined


正確:

function f() {
  return {
    ok: true
  };
}


實作步驟:從需求到可維護的函數

命名清楚:動詞開頭+名詞(getUser, saveOrder)。

單一職責:一個函數只做一件事。

定義介面:參數需求寫清楚(必要/可選),預設值放在參數上。

防禦性檢查:在入口檢驗類型與邏輯。

決定是否純函數:若要可測試、可重用,盡量避免副作用。

錯誤處理:同步用 try/catch,非同步用 try/catch + await 或 promise.catch。

寫測試(至少手動測):列舉邊界情況(空值、負值、極值)。

文件化:註解描述意圖、參數、回傳值、拋出的錯誤。


範例:把金額(分)格式化成人類可讀(元)

/**
 * 將金額(分)轉為字串(元,小數點兩位)
 * @param {number} cents - 必須是整數 >= 0
 * @returns {string} 例如 "123.45"
 */
function formatPrice(cents) {
  if (!Number.isInteger(cents) || cents < 0) {
    throw new TypeError("cents 必須是非負整數");
  }
  const yuan = (cents / 100).toFixed(2);
  return yuan;
}
// 使用
formatPrice(12345); // "123.45"


進階主題:高階函數、閉包、柯里化、函數組合

1.    高階函數(Higher-Order Function)

接收函數或回傳函數:

function times(n, fn) {
  for (let i = 0; i < n; i++) fn(i);
}
times(3, i => console.log(i)); // 0,1,2


2.    閉包(Closure)

內層函數「記住」外層作用域:

function makeCounter(start = 0) {
  let n = start;
  return () => ++n;
}
const c1 = makeCounter();
c1(); // 1
c1(); // 2


3.    柯里化(Currying)

把多參數函數轉為連續一參數函數:

const add = a => b => a + b;
add(2)(3); // 5

// 實務:產生帶前綴的 Logger
const makeLogger = prefix => msg => console.log(`[${prefix}] ${msg}`);
const info = makeLogger("INFO");
info("server started");


4.    函數組合(compose / pipe)

const toUpper = s => s.toUpperCase();
const exclaim = s => s + "!";
const compose = (f, g) => x => f(g(x));
const shout = compose(exclaim, toUpper);
shout("hello"); // "HELLO!"


與非同步的關係:回呼、Promise、async/await

1.    回呼(Callback)

fs.readFile("a.txt", "utf8", (err, data) => {
  if (err) return console.error(err);
  console.log(data);
});


2.    Promise

fetch("/api/user")
  .then(r => r.json())
  .then(data => console.log(data))
  .catch(err => console.error(err));


3.    async/await(語法糖,但更可讀)

async function getUser() {
  try {
    const res = await fetch("/api/user");
    if (!res.ok) throw new Error("HTTP " + res.status);
    return await res.json();
  } catch (err) {
    // 記錄或轉拋
    throw err;
  }
}


雷點: Array.prototype.forEach 搭 async 不會順序等待;改用 for...of:

for (const id of ids) {
  const user = await fetchUser(id);
  console.log(user.name);
}


10.    this、call / apply / bind、方法 vs. 普通函數

1.    this 綁定規則(重點)

一般函數的 this:取決於呼叫點(誰點它)。嚴格模式下,單獨呼叫 this 會是 undefined;非嚴格可能是全域(不建議依賴)。

箭頭函數:沒有自己的 this,用外層作用域的 this。


2.    call / apply / bind

function greet(greeting, name) { console.log(`${greeting}, ${name}`); }
greet.call(null, "Hi", "Ada");
greet.apply(null, ["Hi", "Ada"]);
const hiAda = greet.bind(null, "Hi", "Ada");
hiAda();


3.    方法(Object Method)

const user = {
  name: "Ada",
  say() { console.log(`I'm ${this.name}`); }
};
user.say(); // this 指向 user


雷點: 把 user.say 抽出來當回呼,this 會遺失;用 bind 固定:

const say = user.say.bind(user);
setTimeout(say, 0);


效能與可讀性:純函數、副作用、節流(throttle)/防抖(debounce)

純函數:

        同樣輸入永遠同樣輸出、無副作用(不改外部狀態、不做 I/O)。可測試、可快取。

副作用:

        函數做了外部觀察到的改變(改全域、DOM 操作、網路請求、console…)。必要時請集中管理。


防抖(debounce):停止觸發一段時間後才執行(例如輸入搜尋)。

function debounce(fn, wait = 300) {
  let t;
  return (...args) => {
    clearTimeout(t);
    t = setTimeout(() => fn(...args), wait);
  };
}


節流(throttle):固定間隔才允許執行(例如捲動監聽)。

function throttle(fn, wait = 200) {
  let last = 0;
  return (...args) => {
    const now = Date.now();
    if (now - last >= wait) {
      last = now;
      fn(...args);
    }
  };
}


常見錯誤與雷點

1.    箭頭函數加了 {} 卻忘記 return

const plus = (a, b) => { a + b }; // 回傳 undefined
// 應:const plus = (a, b) => a + b; 或 { return a + b; }


2.    return 換行導致 ASI 插分號(回傳 undefined)

3.    把箭頭函數當建構子用(會噴:Class constructor ... cannot be invoked without 'new' 或類似)

const User = (name) => { this.name = name }; new User("Ada"); // ❌


4.   在箭頭函數裡期待有 arguments(沒有)

      用 ...rest 接參數。

5.    this 指向搞不清:

      把方法拆出來丟回呼導致 this 遺失,請 bind 或改寫成閉包帶入需要的值。

6.    forEach + async 不會等待

       需改 for...of 或 Promise.all。

7.    參數解構但沒預設值

       function f({a}){} 遇到 f(undefined) 會炸;改 function f({a} = {}){}。

8.    預設參數與 null 誤用

       預設只在值是 undefined 時用,傳 null 會被當作合法值。

9.    物件作為預設參數被意外共用狀態

       改成工廠函數回傳新物件。

10.   重複的參數名稱(嚴格模式報錯)

        function f(a, a) {} 在 strict mode 不允許。

11.   把 map + async 當同步在用

        const arr = await list.map(async x => await foo(x)); // arr 是 Promise[]

       需要 await Promise.all(list.map(...))。

12.   沒有處理非同步錯誤

        async 函數要 try/catch;Promise 要 .catch。

13.   忘了回傳 Promise(在封裝時)

        確保回傳 return fetch(...); 或 return Promise.resolve(...)。

14.   把函數當工具亂塞責任

        違反單一職責,導致測試困難、耦合上升。

15.  沒寫 JSDoc / 註解

       下個月的你會感謝現在的你。



範例總表與速查表(Cheat Sheet)

1.    宣告速查

// 宣告
function fn() {}

// 表達式
const fn = function () {};

// 箭頭(單行)
const fn = x => x * 2;

// 箭頭(多行)
const fn = (x, y) => {
  const z = x + y;
  return z;
};

// IIFE
(() => { console.log("run once"); })();


2.    參數與返回

function f(a, b = 1, ...rest) {
  return [a + b, rest];
}
f(3, undefined, 10, 20); // [4, [10, 20]]


3.     解構參數(安全版)

function connect({ host, port } = { host: "localhost", port: 3306 }) {
  return `${host}:${port}`;
}


4.    this 與 bind

const obj = {
  x: 42,
  getX() { return this.x; }
};
const unbound = obj.getX;
unbound(); // undefined (嚴格),或全域(非嚴格,不要依賴)
const bound = obj.getX.bind(obj);
bound(); // 42


5.    高階函數:過濾、映射、歸納

const nums = [1, 2, 3, 4];
nums.filter(n => n % 2 === 0) // [2, 4]
    .map(n => n * 10)         // [20, 40]
    .reduce((a, n) => a + n, 0); // 60


6.    非同步:順序處理與並行處理

// 順序
for (const id of ids) {
  const u = await fetchUser(id);
  console.log(u.id);
}

// 並行
const users = await Promise.all(ids.map(fetchUser));


問題集

Q1:JavaScript 有函數多載(overload)嗎?

沒有語法層級的多載。常見做法是檢查參數型別/數量,在函數內分支處理;或用物件參數加上必要鍵名,避免位置引數混亂。

Q2:為何我在箭頭函數裡拿不到 arguments?

箭頭函數沒有自己的 arguments 與 this。用 ...rest 接收不定數量參數。

Q3:閉包會不會造成記憶體外洩?

閉包本身不是壞事,但長期保留無需要的閉包引用、或把 DOM 節點與大型資料留在閉包裡會增加記憶體壓力。用完的 listener 要解除,長生命週期物件注意清理。

Q4:async 函數一定要回傳 Promise 嗎?

是的,async function 會自動回傳 Promise。你的返回值會被封裝成 Promise.resolve(value)。


附錄:逐步教學(一步步寫一個健壯的查價函數)

需求:給一組商品 ID,向 API 查詢價格,回傳「商品 ID → 價格」的 Map。要求:

        串接 API 錯誤要處理

        支援批次並行

        可設定請求逾時秒數(可選)

        參數要有預設值與檢查

/**
 * 取得商品價格表
 * @param {Object} options
 * @param {string[]} options.ids - 必填,商品 ID 陣列(不可為空)
 * @param {number} [options.timeout=8000] - 請求逾時(毫秒)
 * @returns {Promise<Map<string, number>>}
 */
async function getPriceMap({ ids, timeout = 8000 } = {}) {
  // 1) 防禦性檢查
  if (!Array.isArray(ids) || ids.length === 0) {
    throw new TypeError("ids 必須是非空陣列");
  }
  if (typeof timeout !== "number" || timeout <= 0) {
    throw new TypeError("timeout 必須是正數");
  }

  // 2) 建立可逾時的 fetch
  const withTimeout = (url, ms) => {
    const ctrl = new AbortController();
    const t = setTimeout(() => ctrl.abort(), ms);
    return fetch(url, { signal: ctrl.signal })
      .finally(() => clearTimeout(t));
  };

  // 3) 並行請求(Promise.all)
  try {
    const tasks = ids.map(id =>
      withTimeout(`/api/price?id=${encodeURIComponent(id)}`, timeout)
        .then(r => {
          if (!r.ok) throw new Error(`HTTP ${r.status}`);
          return r.json();
        })
        .then(({ price }) => [id, price]) // 單一結果
    );

    const entries = await Promise.all(tasks); // [ [id, price], ... ]
    return new Map(entries);
  } catch (err) {
    // 4) 錯誤統一處理(可記錄、可轉拋)
    console.error("getPriceMap failed:", err);
    throw err;
  }
}

// 使用
(async () => {
  try {
    const m = await getPriceMap({ ids: ["A1", "B2", "C3"], timeout: 5000 });
    console.log(m.get("A1"));
  } catch (e) {
    // 顯示友善訊息或降級策略
  }
})();


你可以學到:

        物件解構參數+預設值(安全)

        防禦性檢查

        Promise.all 並行

        逾時控制與錯誤統一處理

        回傳結構清晰(Map)


延伸閱讀推薦:



張貼留言 (0)
較新的 較舊