JAVASCRIPT V2 · ADVANCED · 131
Pure Functions / Immutability / Composition:不是炫技,而是降低 Hidden State
Functional patterns 真正實用的地方,不是把程式壓成一行 map/filter,而是讓 transformation 有清楚 input/output、減少意外 mutation,把 side effects 推到 boundary,讓 state transition 可測、可重播、可除錯。
Learning outcomes
- 理解 pure function、side effect、mutation 與 immutability。
- 能把 mutating update 改成 immutable transition。
- 知道 shallow immutability 與 nested reference 的限制。
- 用 intermediate transformations 建立可讀 pipeline。
- 理解 reducer-style state transition 為何容易測試與 replay。
1. Why now:State 越多,Hidden Mutation 越難追
function addStudent(
students,
student
) {
students.push(student);
}
caller 看 function name 可能以為只是「做計算」,但原 array 被改。其他持有同一 reference 的地方也同時受影響。
2. Mental model:Pure Function 是 Input → Output Contract
function addTax(
price,
rate
) {
return price
* (1 + rate);
}
在相同可觀察 input 下得到相同 output,而且 function 本身不修改外部可觀察 state。這讓測試與 reasoning 都更直接。
3. Side Effect 不等於錯
function render(
students
) {
list.replaceChildren(
// DOM nodes
);
}
UI render、network request、storage write 都需要 side effect。策略不是「消滅 side effect」,而是把它集中到明確 boundary。
4. Worked Example A:Immutable Array Update
function addStudent(
students,
student
) {
return [
...students,
student
];
}
const next =
addStudent(
current,
amy
);
console.log(
next === current
);
// false
舊 state 保留,新 state 用新 array identity 表示 transition。
5. Worked Example B:Update One Item
function updateScore(
students,
id,
score
) {
return students.map(
student =>
student.id === id
? {
...student,
score
}
: student
);
}
只有被更新 item 建新 object;沒變的 items 可繼續共享 reference。這叫 structural sharing,不等於 deep clone 全部。
6. Execution trace:Old State → Action → New State
old:
[
{ id: 1, score: 70 },
{ id: 2, score: 80 }
]
action:
update id=1 score=90
new:
[
{ id: 1, score: 90 }, // new object
{ id: 2, score: 80 } // shared object
]
這個 identity 模型和 127 的 reference semantics 直接相連。
7. Reducer-style Transition
function reducer(
state,
action
) {
switch (action.type) {
case "student-added":
return {
...state,
students: [
...state.students,
action.student
]
};
default:
return state;
}
}
Reducer 將「目前 state + action」映射到 next state。React useReducer、Redux 類工具都建立在相似模型上。
8. Worked Example C:Composition 不等於巢狀到看不懂
const normalized =
normalizeStudents(raw);
const passed =
filterPassed(normalized);
const ranked =
sortByScore(passed);
Intermediate variables 能保存每一階段 evidence,debug 時也更容易 inspect。Functional code 不必追求 point-free 或一行完成。
9. Common mistakes
A. 以為 spread = deep immutable
Nested object 仍可能共享 identity。
B. 每次都 JSON deep clone
會丟失特定型別/語意,也有不必要成本。
C. 把所有 side effects 視為不純而禁止
真正做法是隔離 boundary。
D. map/filter chain 過長仍不拆
可讀性與可觀察性比「函數式風格純度」更重要。
10. Debug evidence:Reference Equality 可以證明 Update Boundary
console.log(
next === current
);
console.log(
next[1]
=== current[1]
);
console.log(
next[0]
=== current[0]
);
這些 evidence 能證明哪些層建立新 identity、哪些層共享舊 reference。
11. Guided exercise:Remove without Mutation
function removeStudent(
students,
id
) {
// TODO
}
回傳新 array;不得 splice 原 array。寫測試證明 original length 不變。
12. Independent exercise:Reducer 三種 Action
實作:
student-added
score-updated
student-removed
要求 unknown action 回原 state reference;已處理 action 回合理的新 state。
13. Challenge:Nested Update
const state = {
students: [
{
id: 1,
profile: {
city: "Tainan"
}
}
]
};
只更新 id=1 的 profile.city;說明哪些 identity 應該變、哪些可以共享。
Project checkpoint:Grade Tracker Reducer
let state = {
students: [],
filter: "all"
};
function dispatch(action) {
state =
reducer(
state,
action
);
render(state);
}
State transition 與 render side effect 分開,debug 時可以單獨測 reducer,再確認 render boundary。
14. Vocabulary / Summary
- pure function:輸出由 input 決定,且無隱藏可觀察 side effect。
- mutation:原地改既有 value/object state。
- immutability:以新 value 表達 change,而非改原 value。
- structural sharing:未變部分可共享 reference。
- reducer:state + action → next state。
- side-effect boundary:集中 IO/UI/storage 等外部變化的邊界。
15. Further reading
Knowledge check · Mastery
- 解釋 pure function 與 side-effect boundary。
- 完成 immutable remove、reducer 與 nested update。
- 用 reference equality 證明 structural sharing。
- 完成頁面下方 Executable JS immutable-state tests。