寫出乾淨程式碼!搞懂副作用與純函式的核心概念
更新日期: 2025 年 4 月 22 日
當你在寫 JavaScript 時,你的每一行程式碼,不是「用來算出某個結果」,就是「做出某種行動」。
這兩者的差異,就是程式語言中的「表達式(expression)」與「陳述式(statement)」。
更進一步地,有些程式碼在執行後,會留下「痕跡」,像是改變變數、顯示畫面、送出請求等等——這些就是副作用(side effect)。而當我們希望程式碼能夠乾淨、可預測、不亂改資料時,就會進入「純函式(pure function)」的世界。
本文將一步步帶你掌握這些基本概念,讓你寫出更穩定、好維護的 JavaScript 程式碼。
什麼是表達式(Expression)?
在 JavaScript 中,表達式(Expression) 是「會產生一個值」的程式碼片段。
這個值可以是數字、字串、布林值、物件、函式,甚至是 undefined
或 null
。
🧠 直覺理解:
表達式就像數學中的「算式」,重點在於回傳結果是什麼,而不是執行什麼行為。
✅ 表達式的範例(產生值):
3.14 // ➜ 值為 3.14
"JS" + "學習" // ➜ 值為 "JS學習"
10 - 4 // ➜ 值為 6
5 > 2 // ➜ 值為 true
!false // ➜ 值為 true
(3 + 2) * 4 // ➜ 值為 20
這些都是純計算式,不會改變外部任何變數、畫面或狀態。
✅ 更微妙的表達式(幾乎沒意義):
1;
false;
"hello";
null;
undefined;
這些單獨的值本身就是合法的表達式,雖然它們不「做」任何事情,但它們仍然會被 JS 解譯並回傳值,只是沒有使用到結果時,看起來就像「什麼都沒做」。
❗ 常見誤解:這些不是廢話,它們只是沒被接住或執行邏輯而已,仍是語法正確的表達式。
✅ 巢狀與組合使用:
因為表達式本質上就是「值」,所以它可以巢狀使用:
(1 + 2) * (3 + 4); // 表達式中的表達式 ➜ 最後回傳 21
"Hello " + (1 + 1); // ➜ 回傳 "Hello 2"
也可以作為函式參數:
console.log("總分:" + (50 + 40)); // 表達式放在參數中使用
什麼是陳述式(Statement)?
相對於表達式,陳述式(Statement) 是用來「執行行為、控制程式流程」的指令。它通常是獨立的一行語法,用來控制變數、條件邏輯、迴圈等。
🧠 直覺理解:
陳述式就像是命令你電腦去做一件事,不在乎它會不會回傳值,只在乎它有沒有做完任務。
✅ 常見的陳述式範例:
let name = "JS"; // 宣告與賦值(定義行為)
if (name === "JS") {...} // 條件分支
for (let i = 0; i < 5; i++) {...} // 迴圈
console.log(name); // 呼叫函式、輸出資料
這些都是「指揮」電腦去做某事的語句。它們可能會包含表達式(如 if 的條件),但整體來說,它們是陳述式,負責「流程控制」或「動作執行」。
✅ 特別注意:有些語法既是表達式又是陳述式的組合!
例如:
let x = 1 + 2;
這行整體是「陳述式」(有賦值動作),但右側 1 + 2
則是「表達式」——這展示了兩者的密切關係,但也要能分清楚「誰在做事,誰在算值」。
副作用(Side Effect)是什麼?
在 JavaScript(或其他程式語言)中,當一段程式碼在執行後除了回傳一個值外,還對外部世界造成影響,我們就稱它產生了副作用(Side Effect)。
🧠 直覺理解:
表達式只會算出值,什麼都不會改;
副作用則是「留下痕跡」的程式碼,會對外部狀態或環境產生變化。
✅ 常見的副作用類型
副作用類型 | 行為描述 |
---|---|
改變外部變數 | 修改了函式外部定義的變數 |
修改物件/陣列內容 | 直接改變參數、物件、陣列的內容 |
操作 DOM / 畫面 | 改變 HTML 結構、樣式、動畫等 |
輸出訊息 | 執行 console.log()、alert() |
發送請求 | 執行 fetch()、API 請求 |
操作時間 | 使用 Date.now()、setTimeout() 等 |
寫入/讀取檔案 | 存入本地資料、資料庫、Storage 等 |
依賴外部狀態 | 根據全域變數、環境設定、隨機值來運作 |
這些行為都會導致「執行一次 vs 執行多次」的結果不一定相同,也因此讓程式難以預測、測試與維護。
✅ 具體副作用範例:
let x = 0;
function addAndPrint(n) {
x = x + n; // 改變外部變數 ➜ 副作用
console.log(x); // 印出畫面 ➜ 副作用
}
執行順序與結果:
addAndPrint(2); // 輸出 2
addAndPrint(5); // 輸出 7
這裡的函式有兩個副作用:
- 修改了外部變數
x
- 在畫面上印出結果
每次執行這個函式,都會讓 x
的值越來越大,結果不再只是由輸入決定,也受外部狀態影響。
副作用的風險在哪裡?
副作用本身並不是壞事,但如果沒有妥善管理,在大型應用中會造成以下問題:
問題 | 說明 |
---|---|
不可預測性 | 同樣的函式、同樣的參數,結果可能不同(因為外部狀態不同) |
難以測試 | 測試函式時,必須先設定外部狀態、模擬畫面或 API |
難以重構 | 你無法放心移動或修改有副作用的程式碼,怕影響整個系統 |
debug 困難 | 有副作用的程式碼往往是 bug 的來源,尤其當狀態互相依賴時 |
✅ 無副作用的程式碼長怎樣?
看看這段純計算邏輯:
function add(a, b) {
return a + b;
}
只要給定 a
和 b
,這個函式永遠回傳相同的結果,也不會對任何東西造成影響。
這類程式碼就稱為「純函式」(pure function),是我們接下來要介紹的主角。
🧠 小提醒:副作用不是壞事,但要能「控管」
- 前端程式一定會有副作用(例如畫面變化、用戶輸入、請求伺服器)
- 但我們希望「副作用集中、明確、可控」,例如只在特定時間點處理 DOM 或 API
- React 就是透過
useEffect()
這種機制來「隔離副作用」,維持元件邏輯的清潔
純函式(Pure Function)是什麼?
在程式設計中,「純函式(Pure Function)」是一種非常理想且重要的函式設計方式。它的特色就是「輸入決定輸出,過程不改變外部世界」,像數學公式一樣可預測、可重複、可安心使用。
✅ 純函式的兩大條件:
- 相同輸入,一定產生相同輸出(Deterministic)
➜ 函式不能依賴外部變數、隨機值或時間等不穩定因素。 - 不產生副作用(No Side Effects)
➜ 函式執行不會改變外部變數、操作畫面、印出東西、呼叫 API 等。
只要同時滿足這兩個條件,就是純函式 ✅
✅ 純函式範例:
function add(a, b) {
return a + b;
}
這個函式:
- ✅ 完全只依賴參數
a
和b
- ✅ 不修改任何外部變數
- ✅ 不輸出到畫面
- ✅ 不取時間、不讀取外部資源
所以無論你執行 add(2, 3)
幾次,永遠回傳 5,完全沒有隱藏的行為。這就像數學裡的 f(x) = x + 1
,沒有例外、沒有模糊地帶。
✅ 再看幾個純函式例子:
function square(n) {
return n * n;
}
function formatName(first, last) {
return `${last}, ${first}`;
}
function isEven(n) {
return n % 2 === 0;
}
這些函式都有共同特徵:
- 不依賴外部狀態
- 沒有任何輸出或變數改動
- 結果完全由參數決定
非純函式範例(產生副作用):
let total = 0;
function addToTotal(n) {
total += n; // 改變外部變數 ➜ ❌副作用
}
這個函式會根據外部變數 total
的值改變執行結果。你無法根據 n
的值推論最終結果,因為 total
是隱藏在外部的。
每次執行 addToTotal(3)
,結果都可能不同 ➜ 這違反了「可預測性」。
❌ 其他常見的非純函式例子:
function showAlert(msg) {
alert(msg); // 產生視覺輸出 ➜ 副作用
}
function saveToLocalStorage(key, value) {
localStorage.setItem(key, value); // 改變瀏覽器儲存 ➜ 副作用
}
function getCurrentTime() {
return Date.now(); // 使用外部時間系統 ➜ 非穩定輸出
}
這些函式都會與「外部世界互動」,即使它們可能是必要的,但它們不符合純函式的定義。
純函式有什麼好處?
優點 | 原因說明 |
---|---|
✅ 可預測性 | 輸入一樣 → 結果一定一樣 |
✅ 好測試 | 不用模擬外部世界,只測結果就好 |
✅ 易除錯 | 函式不影響外部狀態,問題更容易定位 |
✅ 可重複使用 | 不依賴特定環境,能放心放進不同地方 |
✅ 可記憶化 | 因為輸出穩定,可以快取結果提升效能(如 useMemo) |
🧠 純函式 ≠ 什麼都不能做!
純函式不是要你放棄畫面互動、API 呼叫,而是教你:
將副作用集中管理,把純邏輯獨立成純函式,讓邏輯可重用、可測試。
例如在 React 中,我們會把副作用放進 useEffect
裡,把資料運算放在純函式中,這樣程式更穩、更好維護。
小結:用乾淨的邏輯思維打造穩定程式
- 表達式專注在「計算與產生值」
- 陳述式則是「做事情與執行流程」
- 副作用代表「會改變世界的程式碼」
- 純函式是「最乾淨、最值得信賴的函式」
當你學會區分這些觀念,就能更清楚知道:
這段程式碼的目的,是用來算東西?還是會改變某個狀態?這會幫助你做出更好維護、更易測試、更有彈性的設計選擇。