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