← JavaScript Learning Path

JAVASCRIPT V2 · EVERYDAY · 59

Spread、Destructuring、Optional Chaining:安全讀寫 Reference Data

這些「現代語法」不是為了讓 code 看起來新。它們主要解決三件事:建立新 object/array 而不是意外共用 reference、清楚取出需要的 properties、在資料可能缺失時安全讀取。理解 shallow copy 與 fallback semantics 比背符號更重要。

Learning outcomes

1. Why now:Reference aliasing 已經開始影響資料更新

const a = { score: 80 };
const b = a;

b.score = 90;

console.log(a.score); // 90

40 已經看到 aliasing。現在開始學「我要更新資料,但保留舊 reference state」的常用工具。

2. Object spread:建立新的 top-level object

const student = {
  name: "Amy",
  score: 80
};

const updated = {
  ...student,
  score: 95
};

student !== updated,而且原本 student.score 仍是 80。

3. Spread 是 shallow copy,不是 deep clone

const user = {
  profile: {
    city: "Tainan"
  }
};

const copy = { ...user };

copy.profile.city = "Taipei";

console.log(user.profile.city);
// Taipei

外層 object 是新的,但 nested profile reference 仍共用。

4. Worked Example A:Nested immutable update

const updated = {
  ...user,
  profile: {
    ...user.profile,
    city: "Taipei"
  }
};

你要在哪一層改 property,就必須確保從 root 到該層需要獨立的 objects 都建立新 reference。

5. Array spread:建立新 array,但 element references 仍可能共用

const oldList = [
  { id: 1, score: 80 }
];

const nextList = [...oldList];

console.log(oldList === nextList);     // false
console.log(oldList[0] === nextList[0]); // true

這就是 shallow copy 的另一個例子。

6. Destructuring:把資料需求寫在左邊

const {
  name,
  score
} = student;
function label({
  name,
  score
}) {
  return name + ": " + score;
}

Destructuring 不會 clone 整個 object,也不會自動 validate properties;它只是建立 local bindings。

7. Rename / Default:面對不同命名或缺省值

const {
  name: displayName,
  score = 0
} = student;

default 只在 property 是 undefined 時使用;不是所有 falsy value 都會 fallback。

8. Optional chaining:安全停止 property access

const city =
  user.profile?.address?.city;

中間某層是 null/undefined 時回 undefined,避免立即 TypeError。

9. ?? vs ||:合法的 0 / false 不應被吃掉

const score = 0;

score || 100; // 100
score ?? 100; // 0

|| 對所有 falsy value fallback;?? 只針對 null/undefined。

10. Worked Example B:安全讀取 + 合理 fallback

const city =
  response.user?.profile?.city
  ?? "Unknown";

這代表「city 缺失可接受並顯示 Unknown」。如果 contract 規定 city 必須存在,就不應用 optional chaining 把錯誤完全藏掉。

11. Worked Example C:Array immutable update

function updateScore(
  students,
  id,
  nextScore
) {
  return students.map(student =>
    student.id === id
      ? {
          ...student,
          score: nextScore
        }
      : student
  );
}

外層 array 是新的,被更新那一筆 object 也是新的;其他未變 records 可以保留原 reference。

12. Common mistakes

A. 以為 spread 是 deep clone

nested reference 仍可能共用。

B. 所有 property access 都加 ?.

可能把 schema regression 變成沉默的 undefined。

C. 用 || fallback 掉合法 0/false

如果只想處理 null/undefined,使用 ??。

D. Destructuring 後誤以為原 object 已改名/修改

只是 local bindings。

13. Debug evidence:檢查 reference equality

console.log({
  sameRoot: oldUser === nextUser,
  sameProfile:
    oldUser.profile === nextUser.profile
});

當「我明明複製了,為什麼舊資料也改了」時,reference equality 是直接 evidence。

14. Guided exercise:判斷哪些 references 是新的

const a = {
  profile: { city: "Tainan" }
};

const b = {
  ...a,
  name: "Amy"
};

回答:a===b?a.profile===b.profile?修改 b.profile.city 會不會影響 a?

15. Independent exercise:Nested update

完成 moveUser(user,nextCity):回傳新 root object、新 profile object,其他欄位保留,原 user 不變。

16. Challenge:Update by ID

用 map + spread 更新 Student[] 中指定 id 的 score,並確認:

Project checkpoint:Grade Tracker Immutable Update

function updateStudent(
  students,
  id,
  patch
) {
  return students.map(student =>
    student.id === id
      ? { ...student, ...patch }
      : student
  );
}

這個 pattern 會直接進 React state,因此現在先把 reference semantics 學穩。

17. Vocabulary / Summary

18. Further reading

Knowledge check

  1. 說明 shallow copy 為什麼不會複製 nested object。
  2. 比較 ?? 與 || 對 0 的結果。
  3. 說明 optional chaining 解決的是 crash 還是 validity。
  4. 用 reference equality 驗證 nested update 是否真的建立新 object。
  5. 完成 nested immutable update mastery tests。