好維護的程式碼-Day04-函式該切多細講函式的邊界該切在哪。切好邊界之後,下一個問題是:這個函式跟外界溝通的窗口——參數列——該長什麼樣子?
今天從一個幾乎每個後端都寫過的函式開始。
需求:建立使用者,可能是管理員,可能要寄歡迎信,可能要跳過驗證(例如批次匯入舊資料時)。
function createUser(name, email, password, isAdmin, sendWelcomeEmail, skipValidation) {
return {
name,
email,
password: skipValidation ? password : `hashed(${password})`,
isAdmin: !!isAdmin,
welcomeEmailQueued: !!sendWelcomeEmail,
}
}
呼叫端長這樣:
createUser('Abby', 'abby@example.com', 'pw123', false, true, false)
這段程式碼完全正確。但它有三個維護成本,而且最危險的那個不是「讀起來麻煩」,是它會悄悄做錯事,卻不會有任何錯誤訊息。
看到 createUser('Abby', 'abby@example.com', 'pw123', false, true, false),你能不看函式定義就講出第四、五、六個參數各是什麼意思嗎?
大部分人不行。三個連續的布林值,語意完全靠位置記憶,讀的人得回頭對照函式簽名,數到第幾個位置才知道 true 代表什麼。
這是今天最重要的一點,直接實測。
// 本來想表達:isAdmin=false, sendWelcomeEmail=true
createUser('Hacker', 'h@example.com', 'pw', true, false, false)
// ^^^^ ^^^^^
// 不小心寫反了順序
實測結果:
| 想要的結果 | 實際結果 |
|---|---|
isAdmin: false |
isAdmin: true ❌ |
welcomeEmailQueued: true |
welcomeEmailQueued: false ❌ |
這行程式碼語法完全合法,型別檢查(如果只用一般 JS)完全通過,沒有任何工具會警告你。 它會安安靜靜地把一個一般使用者升級成管理員。三個相鄰的布林參數,就是三個可以互相調換、且調換後依然「看起來沒錯」的陷阱。
如果只想指定 sendWelcomeEmail,跳過 isAdmin,位置參數逼你這樣寫:
createUser('Bob', 'bob@example.com', 'pw', undefined, true, undefined)
兩個 undefined 純粹是為了佔住位置,沒有任何語意,只是「因為它在中間,所以不能不寫」。
function createUser({
name,
email,
password,
isAdmin = false,
sendWelcomeEmail = true,
skipValidation = false,
}) {
return {
name,
email,
password: skipValidation ? password : `hashed(${password})`,
isAdmin: !!isAdmin,
welcomeEmailQueued: !!sendWelcomeEmail,
}
}
呼叫端:
createUser({
name: 'Abby',
email: 'abby@example.com',
password: 'pw123',
isAdmin: false,
sendWelcomeEmail: true,
})
同樣的「順序寫反」實驗,這次把整個 options 物件的 key 順序打亂:
createUser({ sendWelcomeEmail: true, name: 'Hacker', isAdmin: false, email: 'h@example.com', password: 'pw' })
實測結果依然正確——isAdmin: false、welcomeEmailQueued: true,跟預期一致。因為每個值都綁著名字,不是綁著位置,順序再怎麼打亂都不影響結果。
省略可選參數也不再需要佔位:
createUser({ name: 'Bob', email: 'bob@example.com', password: 'pw', sendWelcomeEmail: true })
只寫你關心的 key,其餘用預設值,不用數 undefined。
寫這篇之前我的直覺是「多一層解構,應該會慢一點」,所以實測了 200 萬次呼叫。
| 版本 | 200 萬次呼叫耗時 |
|---|---|
位置參數版(fn(...args)) |
95.0 ms |
Options 版(fn(opts)) |
44.7 ms |
結果跟我的直覺相反:Options 版反而快了約 53%。 我原本猜錯了,老實講出來——重新檢查後,這個差距主要不是「物件解構比較快」造成的,而是位置參數版用了 fn(...args) 展開陣列呼叫,展開語法本身有它自己的開銷,這讓比較沒有完全對等。
但結論依然成立,只是理由要修正:不管哪個版本比較快,兩者的差距在單次呼叫的尺度上是奈秒等級,在真實應用裡完全可忽略。V8 對物件解構有做最佳化,這不是一個該拿來決定寫法的因素。除非你在寫每秒百萬次呼叫的熱路徑(例如 game loop 或即時訊號處理),效能都不該是選 Options Object 與否的理由。
不是。
Math.max(a, b)
array.map(fn, thisArg)
這種函式參數少、順序本身就是慣例(max(a, b) 誰先誰後根本不影響結果),硬包成 Math.max({ a, b }) 反而是過度包裝,多一層不必要的物件建立成本,也不符合語言慣例。
a. 參數少且語意從位置就看得出來
function add(a, b) { return a + b }
兩個對等的數字,包成物件反而畫蛇添足。
b. 高頻呼叫的效能敏感路徑
雖然上面實測差距可忽略,但如果 profiler 真的指出這裡是瓶頸,先量測、再決定,不要單憑這篇文章的結論。
c. 參數彼此有嚴格順序依賴的 DSL / builder
某些鏈式 API 或 DSL 設計上就是刻意用位置表達順序(例如 pipe(fn1, fn2, fn3)),這種情況位置本身就是語意的一部分,不該硬拗成物件。
「呼叫這個函式時,你需要不需要回頭看它的定義,才知道每個值是什麼意思?」
| 情況 | 建議 |
|---|---|
| 參數數量 ≤ 2,且語意從位置就看得出來 | 保持位置參數 |
參數數量 > 3(max-params 的預設門檻) |
改用 Options Object |
| 有 2 個以上相鄰且同型別的參數(尤其是布林值) | 不管總數多少,都該改用 Options Object——這是順序寫反最容易發生的地方 |
| Day 4 | Day 5 | |
|---|---|---|
| 處理的邊界 | 函式該不該存在(職責) | 函式怎麼跟外界溝通(介面) |
| 判準 | 碰幾種外部系統 | 需不需要回頭看定義才懂參數 |
| 共同點 | 都是把「隱含的規則」變成「看得見的名字」 |
Day 6 講布林變數命名:isValid 為什麼比 valid 好?isNotDisabled 這種雙重否定的命名,為什麼會讓判斷式變成猜謎?
一、有正式出處的部分
| 內容 | 出處 |
|---|---|
| Introduce Parameter Object(把一串參數包成物件)這個重構手法 | Martin Fowler, Refactoring: Improving the Design of Existing Code, 2nd ed. |
max-params 規則與預設門檻(3 個) |
ESLint 官方文件:https://eslint.org/docs/latest/rules/max-params |
| 「參數與變數設計」主題架構 | ExplainThis 線上課程《寫出好維護的程式碼 (下)》CH4:https://www.explainthis.io/zh-hant/courses/maintainable-code-part2(付費課程,本文未逐字引用內容,僅取主題方向) |
| 解構賦值、預設參數語法 | MDN, Destructuring assignment:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment;MDN, Default parameters:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Default_parameters |
二、我實際跑出來的部分
順序寫反的行為對照、省略可選參數的對照、200 萬次呼叫的效能量測,全部由 day05-options-object.js 實測產生,可重跑驗證。實測環境 Node.js v24.14.0。效能量測的解讀在文中已修正:Options 版實測較快,但主因可能是位置參數版的展開語法(...args)開銷,並非物件解構本身更快;兩者差距在真實應用中都可忽略。
三、我自己的整理與比喻(沒有外部出處)
(查閱日期:2026-08-24。本文所有數字實測於 Node.js v24.14.0,可執行腳本見文末)
本文所有對照與效能數字,都可以用這支腳本重跑驗證:day05-options-object.js(順序寫反行為對照、省略參數對照、200 萬次呼叫效能量測)。