← JavaScript Learning Path

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

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

15. Further reading

Knowledge check · Mastery

  1. 解釋 pure function 與 side-effect boundary。
  2. 完成 immutable remove、reducer 與 nested update。
  3. 用 reference equality 證明 structural sharing。
  4. 完成頁面下方 Executable JS immutable-state tests。