寫出乾淨程式碼!搞懂副作用與純函式的核心概念

更新日期: 2025 年 4 月 22 日

可開啟 CC 字幕搭配中文翻譯

當你在寫 JavaScript 時,你的每一行程式碼,不是「用來算出某個結果」,就是「做出某種行動」。

這兩者的差異,就是程式語言中的「表達式(expression)」與「陳述式(statement)」。

更進一步地,有些程式碼在執行後,會留下「痕跡」,像是改變變數、顯示畫面、送出請求等等——這些就是副作用(side effect)。而當我們希望程式碼能夠乾淨、可預測、不亂改資料時,就會進入「純函式(pure function)」的世界。

本文將一步步帶你掌握這些基本概念,讓你寫出更穩定、好維護的 JavaScript 程式碼。


什麼是表達式(Expression)?

在 JavaScript 中,表達式(Expression) 是「會產生一個值」的程式碼片段。

這個值可以是數字、字串、布林值、物件、函式,甚至是 undefinednull

🧠 直覺理解:

表達式就像數學中的「算式」,重點在於回傳結果是什麼,而不是執行什麼行為。

✅ 表達式的範例(產生值):

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

這裡的函式有兩個副作用:

  1. 修改了外部變數 x
  2. 在畫面上印出結果

每次執行這個函式,都會讓 x 的值越來越大,結果不再只是由輸入決定,也受外部狀態影響

副作用的風險在哪裡?

副作用本身並不是壞事,但如果沒有妥善管理,在大型應用中會造成以下問題:

問題說明
不可預測性同樣的函式、同樣的參數,結果可能不同(因為外部狀態不同)
難以測試測試函式時,必須先設定外部狀態、模擬畫面或 API
難以重構你無法放心移動或修改有副作用的程式碼,怕影響整個系統
debug 困難有副作用的程式碼往往是 bug 的來源,尤其當狀態互相依賴時

✅ 無副作用的程式碼長怎樣?

看看這段純計算邏輯:

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

只要給定 ab,這個函式永遠回傳相同的結果,也不會對任何東西造成影響

這類程式碼就稱為「純函式」(pure function),是我們接下來要介紹的主角。

🧠 小提醒:副作用不是壞事,但要能「控管」

  • 前端程式一定會有副作用(例如畫面變化、用戶輸入、請求伺服器)
  • 但我們希望「副作用集中、明確、可控」,例如只在特定時間點處理 DOM 或 API
  • React 就是透過 useEffect() 這種機制來「隔離副作用」,維持元件邏輯的清潔

純函式(Pure Function)是什麼?

在程式設計中,「純函式(Pure Function)」是一種非常理想且重要的函式設計方式。它的特色就是「輸入決定輸出,過程不改變外部世界」,像數學公式一樣可預測、可重複、可安心使用。

✅ 純函式的兩大條件:

  1. 相同輸入,一定產生相同輸出(Deterministic)
    ➜ 函式不能依賴外部變數、隨機值或時間等不穩定因素。
  2. 不產生副作用(No Side Effects)
    ➜ 函式執行不會改變外部變數、操作畫面、印出東西、呼叫 API 等。

只要同時滿足這兩個條件,就是純函式 ✅

✅ 純函式範例:

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

這個函式:

  • ✅ 完全只依賴參數 ab
  • ✅ 不修改任何外部變數
  • ✅ 不輸出到畫面
  • ✅ 不取時間、不讀取外部資源

所以無論你執行 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 裡,把資料運算放在純函式中,這樣程式更穩、更好維護。


小結:用乾淨的邏輯思維打造穩定程式

  • 表達式專注在「計算與產生值
  • 陳述式則是「做事情與執行流程
  • 副作用代表「會改變世界的程式碼
  • 純函式是「最乾淨、最值得信賴的函式

當你學會區分這些觀念,就能更清楚知道:

這段程式碼的目的,是用來算東西?還是會改變某個狀態?這會幫助你做出更好維護、更易測試、更有彈性的設計選擇。

Similar Posts