Skip to content

Lesson 0.6.8 — Spread & Shallow Copy ​

Bài 0.6.8 — Spread & Shallow Copy
Object spread, Array spread & Reference sharing • 30 phút
0:00 / 0:00

0. Metadata ​

FieldValue
Stage0 — JavaScript Language Foundation
Module0.6 — Data Structures
Lesson0.6.8
CompetencyC01.6 — Arrays / Objects
Depth TargetL2–L3
PrerequisitesObjects (0.6.5), Arrays (0.6.2), Destructuring (0.6.7)
Estimated Cognitive LoadMedium–High

1. Why This Exists (Vì sao cần học) ​

Bạn đã biết cách truy cập và "mở" object/array bằng destructuring. Nhưng trong production, một vấn đề phổ biến hơn nhiều là: làm sao để tạo một object/array mới dựa trên dữ liệu cũ mà không làm hỏng dữ liệu gốc?

Xét đoạn code:

js
const user = { name: "Alice", role: "user" };
const updated = user;
updated.role = "admin";
console.log(user.role); // "admin" ❌

Vấn đề cốt lõi

updated = user không tạo ra object mới. Nó tạo thêm một binding trỏ đến cùng một object. Khi bạn sửa updated, bạn đang sửa user.

Spread syntax (...) giải quyết điều này bằng cách tạo một object/array mới, sao chép các property cũ vào. Nhưng có một điểm mù nguy hiểm: spread chỉ shallow copy — nếu object chứa object lồng bên trong, reference bên trong vẫn được chia sẻ.

Bài này dạy bạn dùng spread như một công cụ immutable update: tạo mới từ cũ, thay đổi có chọn lọc, và nhận biết giới hạn shallow copy trước khi nó gây bug trong state management.

2. Prerequisites (Yêu cầu đầu vào) ​

Trước khi học bài này, bạn cần:

  • Biết object là reference value, khác primitive (0.2.1, 0.6.5).
  • Biết cách truy cập, thêm, sửa, xóa property object/array (0.6.2, 0.6.5).
  • Hiểu destructuring và rest pattern ...rest (0.6.7).
  • Phân biệt const binding với object mutation (0.2.2).

WARNING

Nếu bạn chưa chắc tại sao const a = {}; const b = a; b.x = 1 khiến a.x cũng bằng 1, quay lại 0.2.1 (Values) và 0.6.5 (Objects). Spread không thay thế được việc hiểu reference.

3. Learning Objectives (Mục tiêu học tập) ​

Sau bài này, bạn có thể:

  1. Sử dụng object spread ({ ...obj }) để tạo shallow copy của object.
  2. Sử dụng array spread ([ ...arr ]) để tạo shallow copy của array.
  3. Kết hợp spread với property override để update object immutably.
  4. Sử dụng spread để merge nhiều object/array.
  5. Phân biệt shallow copy và deep copy ở mức khái niệm.
  6. Nhận diện mutation bug khi spread object có nested object.
  7. Sử dụng spread arguments (fn(...args)) để truyền array làm argument riêng lẻ.

4. Mental Model (Mô hình tư duy) ​

Mental Model

Spread = Chụp ảnh surface, không chụp ảnh bên trong

Object Spread: { ...user, role: "admin" }
  ↓
  Tạo object MỚI
  ↓
  Copy từng property của user vào object mới (shallow)
  ↓
  Ghi đè role = "admin"
  ↓
  user (cũ) không đổi

Array Spread: [ ...items, "new" ]
  ↓
  Tạo array MỚI
  ↓
  Copy từng phần tử (shallow)
  ↓
  Thêm "new" vào cuối
  ↓
  items (cũ) không đổi

Shallow Copy Limit
  ↓
  Nếu property là object: chỉ copy REFERENCE
  ↓
  Object lồng bên trong vẫn dùng chung với bản gốc

Quy tắc vàng:

  • Spread tạo container mới, nhưng không tạo bản sao độc lập của nội dung bên trong.
  • Object spread copy own enumerable properties.
  • Array spread copy theo iteration order (index 0, 1, 2...).
  • Property sau ghi đè property trước khi trùng key.

5. Core Concepts (Các khái niệm cốt lõi) ​

Essential (Bắt buộc) ​

ConceptÝ nghĩa
Object Spread{ ...obj } — tạo object mới, copy property từ obj.
Array Spread[ ...arr ] — tạo array mới, copy phần tử từ arr.
Spread + Override{ ...obj, key: newValue } — copy rồi ghi đè một property.
Merge{ ...a, ...b } — gộp property từ nhiều object. Key sau thắng.
Spread Argumentsfn(...args) — trải array thành argument list.
Shallow CopyCopy container mới, nhưng nested object vẫn share reference.

Supporting (Hỗ trợ) ​

ConceptÝ nghĩa
Object.assignObject.assign({}, obj) — tương đương { ...obj }. Spread syntax ngắn gọn hơn.
Array concat[...a, ...b] thay thế a.concat(b).

Awareness (Biết tồn tại) ​

ConceptLý do chưa đào sâu
Deep CloneCần recursive copy hoặc structuredClone. Thuộc Stage 2 (Object Model) và Stage 11 (Memory).
Sparse array[...arr] bỏ qua empty slots, tạo undefined thật. Chi tiết ở Stage 2.
Iterator protocolArray spread dựa trên iterator. Thuộc Stage 2.

Out of Scope (Không thuộc bài này) ​

  • Implement deep clone từ đầu.
  • structuredClone (Web API, Stage 4/11).
  • Spread trong TypeScript type (Stage 6).
  • React state immutability pattern (Stage 8 — preview có thể nhưng không đào sâu).

6. Worked Example (Ví dụ phân tích từng bước) ​

Bài toán: Cập nhật user profile mà không mutate object gốc.

js
const user = {
  id: 42,
  name: "Alice",
  profile: {
    role: "user",
    theme: "light"
  }
};

Yêu cầu: Tạo updatedUser với role: "admin" và theme: "dark", nhưng user phải giữ nguyên.

js
const updatedUser = user;
updatedUser.profile.role = "admin";
updatedUser.profile.theme = "dark";
// user cũng bị đổi ❌
js
const updatedUser = {
  ...user,
  profile: {
    role: "admin",
    theme: "dark"
  }
};
// user giữ nguyên ✅
// Nhưng profile cũ bị thay bằng object mới hoàn toàn
js
const updatedUser = {
  ...user,
  profile: {
    ...user.profile,
    role: "admin",
    theme: "dark"
  }
};
// user giữ nguyên ✅
// user.profile giữ nguyên ✅
// Chỉ profile mới có giá trị khác

Walkthrough

Step 1 — Reference Assignment (updatedUser = user)

  • Observation: updatedUser được gán trực tiếp từ user.
  • Classification: Đây là reference assignment, không phải value copy.
  • Reasoning: Cả hai biến trỏ đến cùng một object trong memory. JavaScript không tự động clone object khi gán.
  • Conclusion: Sửa updatedUser.profile.role sẽ sửa luôn user.profile.role vì cùng target. Đây là mutation bug phổ biến nhất.

Step 2 — Object Spread ({ ...user })

  • Observation: { ...user } tạo ra một object mới với các property trùng tên.
  • Classification: Top-level properties (id, name) được copy by value (primitive). profile là object nên copy by reference.
  • Reasoning: Spread syntax (...) copy own enumerable properties từ source sang target. Với profile, giá trị được copy là reference (địa chỉ memory) chứ không phải bản sao độc lập của object { role, theme }.
  • Conclusion: updatedUser.profile và user.profile vẫn trỏ cùng một nested object. Sửa nested property sẽ ảnh hưởng cả hai. Để tránh, cần nested spread.

Step 3 — Override profile{ ...user, profile: { ... } } tạo một profile object hoàn toàn mới. user.profile không bị ảnh hưởng. Nhưng ta mất các property cũ của profile nếu không copy lại.

Step 4 — Nested Spread ({ ...user.profile }) Copy toàn bộ property từ user.profile vào object profile mới, rồi ghi đè role và theme. Kết quả: user không đổi, user.profile không đổi, chỉ updatedUser.profile là object mới với giá trị mong muốn.

Key Insight: Spread là shallow. Muốn immutable update nested, phải spread từng cấp. Đây là lý do tại sao state management libraries (Redux, Zustand, React) yêu cầu "immutability" — và cũng là lý do tại sao người ta dùng libraries như Immer (Stage 8).

7. Prediction Exercise (Bài tập dự đoán) ​

Đừng chạy code. Đọc và dự đoán output, sau đó giải thích tại sao.

Câu 1 ​

js
const a = { x: 1, y: 2 };
const b = { ...a };
b.x = 99;
console.log(a.x);

Câu 2 ​

js
const a = { nested: { value: 10 } };
const b = { ...a };
b.nested.value = 99;
console.log(a.nested.value);

Câu 3 ​

js
const defaults = { theme: "light", lang: "en" };
const user = { lang: "vi" };
const settings = { ...defaults, ...user };
console.log(settings.theme, settings.lang);

Câu 4 ​

js
const nums = [1, 2];
const more = [...nums, 3, ...nums];
console.log(more);

Câu 5 ​

js
const a = { x: 1 };
const b = { x: 2, y: 3 };
const c = { ...b, ...a };
console.log(c.x, c.y);

Yêu cầu: Dự đoán output và giải thích tại sao thứ tự spread quyết định kết quả. Nếu đổi thành { ...a, ...b }, c.x sẽ là bao nhiêu?

Câu 6 (Transfer) ​

js
const settings = { theme: "dark", notifications: true };
const prefs = { fontSize: 14 };

const merged = { ...settings, ...prefs, theme: "light" };
console.log(merged.theme, merged.notifications, merged.fontSize);

Yêu cầu: Dự đoán output và giải thích thứ tự spread quyết định kết quả như thế nào. Nếu đổi thành { ...prefs, ...settings, theme: "light" }, kết quả khác gì?

[Đáp án & Giải thích]

Câu 1: 1 — Shallow copy top-level

  • Giải thích: { ...a } tạo object mới, copy x và y. b.x = 99 sửa property trên object mới. a.x không đổi.

Câu 2: 99 — Nested reference shared

  • Giải thích: { ...a } copy property nested. Nhưng nested là reference trỏ đến cùng một object { value: 10 }. b.nested.value = 99 sửa object được cả a và b chia sẻ. Shallow copy không bảo vệ nested mutation.

Câu 3: "light" "vi" — Merge + override

  • Giải thích: ...defaults copy theme: "light", lang: "en". Sau đó ...user ghi đè lang: "vi". Key sau thắng key trước.

Câu 4: [1, 2, 3, 1, 2] — Array spread + concat

  • Giải thích: ...nums trải [1, 2]. more là [1, 2, 3, 1, 2]. Array gốc không đổi.

Câu 5: 1 3 — Override order

  • Giải thích: ...b copy x: 2, y: 3. ...a copy x: 1, ghi đè x trước đó. y giữ nguyên 3. Thứ tự spread quyết định kết quả.

Câu 6: "light" true 14

  • Giải thích:
    • ...settings copy theme: "dark", notifications: true.
    • ...prefs copy fontSize: 14.
    • theme: "light" ghi đè theme trước đó.
      Nếu đổi thứ tự: { ...prefs, ...settings, theme: "light" } vẫn cho theme: "light" vì property sau cùng vẫn là theme: "light", nhưng nếu settings có fontSize thì ...settings sẽ ghi đè ...prefs. Key sau thắng key trước trong cùng một object literal.

8. Implementation Lab (Bài lab thực hành) ​

Level 1 — Guided (Có hướng dẫn) ​

Viết function toggleTodo nhận todo object và trả về object mới với completed đảo ngược, không mutate bản gốc.

js
const todo = { id: 1, text: "Learn spread", completed: false };

function toggleTodo(todo) {
  return _________;
}

console.log(toggleTodo(todo));
// { id: 1, text: "Learn spread", completed: true }
console.log(todo.completed);
// false (không đổi)

Gợi ý

Dùng object spread: { ...todo, completed: !todo.completed }.

[Đáp án tham khảo]
js
function toggleTodo(todo) {
  return { ...todo, completed: !todo.completed };
}

const todo = { id: 1, text: "Learn spread", completed: false };

const updated = toggleTodo(todo);
console.log(updated);
// { id: 1, text: "Learn spread", completed: true }

console.log(todo.completed);
// false

Giải thích:

  • { ...todo } tạo object mới với toàn bộ property của todo.
  • completed: !todo.completed ghi đè property completed trên object mới.
  • todo gốc không bị ảnh hưởng.

Level 2 — Partial Scaffold (Khung sẵn) ​

Hoàn thành function updateUser dùng nested spread để update profile.theme mà không mutate object gốc.

js
function updateUser(user, newTheme) {
  return {
    _______,
    profile: {
      _______,
      theme: _______
    }
  };
}

const user = {
  id: 1,
  name: "Alice",
  profile: { role: "user", theme: "light" }
};

const updated = updateUser(user, "dark");
console.log(updated.profile.theme); // "dark"
console.log(user.profile.theme);    // "light"
[Đáp án tham khảo]
js
function updateUser(user, newTheme) {
  return {
    ...user,
    profile: {
      ...user.profile,
      theme: newTheme
    }
  };
}

const user = {
  id: 1,
  name: "Alice",
  profile: { role: "user", theme: "light" }
};

const updated = updateUser(user, "dark");
console.log(updated.profile.theme); // "dark"
console.log(user.profile.theme);    // "light"

Giải thích:

  • ...user copy toàn bộ property top-level.
  • profile: { ...user.profile, theme: newTheme } tạo profile mới, copy role cũ, ghi đè theme.
  • user.profile vẫn là object cũ, không đổi.

Level 3 — Independent (Tự viết) ​

Viết các function sau sử dụng spread:

js
// 1. prepend(arr, value) → Trả về array mới với value ở đầu
//    Ví dụ: prepend([2, 3], 1) → [1, 2, 3]

// 2. mergeSettings(defaults, overrides) → Gộp hai object, overrides thắng khi trùng key
//    Ví dụ: mergeSettings({ a: 1, b: 2 }, { b: 3, c: 4 }) → { a: 1, b: 3, c: 4 }

// 3. addTag(post, tag) → post có { id, title, tags: ["js"] }
//    Trả về post mới với tag được thêm vào cuối tags, không mutate
//    Ví dụ: addTag({ id: 1, title: "A", tags: ["js"] }, "react")
//    → { id: 1, title: "A", tags: ["js", "react"] }

// Sử dụng:
console.log(prepend(["b", "c"], "a"));
// ["a", "b", "c"]

console.log(mergeSettings({ theme: "light" }, { theme: "dark", lang: "vi" }));
// { theme: "dark", lang: "vi" }

const post = { id: 1, title: "Hello", tags: ["js"] };
console.log(addTag(post, "react"));
// { id: 1, title: "Hello", tags: ["js", "react"] }
console.log(post.tags);
// ["js"] (không đổi)
[Đáp án tham khảo]
js
function prepend(arr, value) {
  return [value, ...arr];
}

function mergeSettings(defaults, overrides) {
  return { ...defaults, ...overrides };
}

function addTag(post, tag) {
  return {
    ...post,
    tags: [...post.tags, tag]
  };
}

console.log(prepend(["b", "c"], "a"));
// ["a", "b", "c"]

console.log(mergeSettings({ theme: "light" }, { theme: "dark", lang: "vi" }));
// { theme: "dark", lang: "vi" }

const post = { id: 1, title: "Hello", tags: ["js"] };
console.log(addTag(post, "react"));
// { id: 1, title: "Hello", tags: ["js", "react"] }
console.log(post.tags);
// ["js"]

Giải thích:

  • prepend: [value, ...arr] tạo array mới, value ở đầu, copy phần còn lại.
  • mergeSettings: ...defaults trước, ...overrides sau. Key trùng bị ghi đè bởi overrides.
  • addTag: ...post copy top-level. tags: [...post.tags, tag] tạo array tags mới từ array cũ + thêm phần tử. post.tags không đổi.

Level 4 — Spread Arguments ​

Viết function invoke nhận một function fn và một array args, sau đó gọi fn với các phần tử của args làm argument riêng lẻ. Không dùng fn.apply.

js
function invoke(fn, args) {
  return _______;
}

function sum(a, b, c) {
  return a + b + c;
}

console.log(invoke(sum, [1, 2, 3])); // 6
console.log(invoke(Math.max, [5, 2, 9, 1])); // 9
[Đáp án tham khảo]
js
function invoke(fn, args) {
  return fn(...args);
}

Giải thích: ...args trải array thành danh sách argument riêng lẻ. Đây là cách thay thế fn.apply(null, args) trong ES6+.

9. Edge Cases (Các trường hợp ngoại lệ) ​

Ghi chú: Phân loại 2 edge cases đầu là Critical (dễ gây bug production), 2 edge cases sau là Awareness (biết để tránh lỗi lạ, không cần thuộc ở L2–L3).

CRITICAL — Shallow copy không bảo vệ nested mutation

js
const a = { inner: { value: 1 } };
const b = { ...a };
b.inner.value = 2;
console.log(a.inner.value); // 2

Tại sao: inner là reference. Spread copy reference, không copy object bên trong.

Cách nhận biết: Sau spread, sửa nested property — object gốc cũng đổi.

Cách xử lý: Nested spread từng cấp, hoặc dùng deep clone utility (Stage 2/11).

CRITICAL — Property sau ghi đè property trước

js
const a = { x: 1 };
const b = { x: 2 };
const c = { ...a, ...b, x: 3 };
console.log(c.x); // 3

Tại sao: Spread copy theo thứ tự từ trái sang phải. Key trùng bị ghi đè liên tục.

Cách nhận biết: Kết quả merge khác mong đợi do thứ tự object.

Cách xử lý: Sắp xếp thứ tự spread đúng: defaults trước, overrides sau.

AWARENESS — Array sparse behavior (Stage 2)

js
const a = [1, , 3];
const b = [...a];
console.log(0 in b, 1 in b);
// true true (1 in b là true vì b[1] === undefined)

Tại sao: Spread iterate qua array, tạo phần tử undefined thật cho empty slots. Khác với sparse array gốc.

Cách nhận biết: in operator hoặc hasOwnProperty cho kết quả khác sau spread.

Cách xử lý: Tránh dựa vào sparse array semantics. Nếu cần sparse, dùng index assignment trực tiếp (hiếm gặp trong production).

AWARENESS — Spread null/undefined throw trong object context (không phải array)

js
const a = null;
const b = { ...a }; // ✅ {} — object spread cho phép null/undefined
const c = [...a];   // TypeError: a is not iterable

Tại sao: Object spread gọi internal ToObject nên xử lý được null. Array spread yêu cầu iterable.

Cách nhận biết: TypeError khi spread array từ giá trị không iterable.

Cách xử lý: Guard clause: [...(arr || [])].

10. Debug Lab (Bài lab gỡ lỗi) ​

Symptom (Triệu chứng): Sau khi cập nhật một item trong danh sách, toàn bộ danh sách cũ cũng bị thay đổi.

Reproduction (Tái hiện lỗi):

js
const users = [
  { id: 1, name: "Alice", profile: { role: "user" } },
  { id: 2, name: "Bob", profile: { role: "admin" } }
];

function updateRole(users, id, newRole) {
  return users.map(user => {
    if (user.id === id) {
      const copy = { ...user };
      copy.profile.role = newRole; // Bug!
      return copy;
    }
    return user;
  });
}

Evidence (Bằng chứng):

  • copy = { ...user } tạo object mới nhưng profile vẫn là reference cũ.
  • copy.profile.role = newRole sửa object profile được cả user và copy chia sẻ.
  • users[0].profile.role đổi thành "admin" dù object gốc users không được reassign.

Hypothesis (Giả thuyết): { ...user } chỉ thực hiện shallow copy. Property profile là một object reference — spread copy reference chứ không tạo object profile mới. Do đó copy.profile trỏ cùng một object với user.profile.

Verification (Xác minh):

js
console.log(copy.profile === user.profile); // true → chứng minh cùng reference

Root Cause (Nguyên nhân gốc rễ): Hiểu nhầm spread tạo deep copy. Thực tế spread chỉ shallow copy top-level properties. Nested object vẫn share reference.

Fix (Sửa):

js
function updateRole(users, id, newRole) {
  return users.map(user => {
    if (user.id === id) {
      return {
        ...user,
        profile: {
          ...user.profile,
          role: newRole
        }
      };
    }
    return user;
  });
}

Prevention (Phòng ngừa):

  • Khi dùng spread để immutable update, luôn hỏi: "Có property nào là object/array không?"
  • Nếu có, spread từng cấp hoặc dùng library (Immer, Stage 8).
  • Viết test kiểm tra object gốc sau update.

11. Design Exercise (Bài tập thiết kế giải pháp) ​

Bạn cần viết function createConfig nhận baseConfig và envOverrides.

js
function createConfig(base, overrides) {
  return { ...base, ...overrides };
}
js
function createConfig(base, overrides) {
  return {
    host: overrides.host || base.host,
    port: overrides.port ?? base.port,
    debug: overrides.debug !== undefined ? overrides.debug : base.debug
  };
}

Câu hỏi:

  1. Option A có lợi gì khi base có 20 property?
  2. Option B có lợi gì về type safety và validation?
  3. Nếu overrides chứa property lạ không thuộc base (ví dụ hacker: true), option nào nguy hiểm hơn?
[Đáp án tham khảo]
  • Bạn nghĩ:

    1. Option A: Ngắn gọn, tự động merge mọi property. Không cần sửa code khi thêm field mới vào base.
    2. Option B: Explicit, dễ đọc, dễ thêm validation từng field. ?? phân biệt null và undefined. Không cho phép property lạ lọt vào.
    3. Option A nguy hiểm hơn: ...overrides copy mọi property, kể cả unexpected. Option B chỉ copy những gì được liệt kê.
  • Kết luận: Spread merge phù hợp cho internal/controlled data. Explicit pick phù hợp cho external/untrusted input (API, CLI args). Đây là trade-off giữa convenience và safety.

12. Production Scenario (Tình huống thực tế) ​

Context: Bạn đang làm feature "Duplicate Post" trong CMS.

js
const original = {
  id: 101,
  title: "Hello World",
  meta: {
    tags: ["news"],
    author: { id: 5, name: "Alice" }
  }
};

function duplicatePost(post) {
  return { ...post, id: null };
}

Symptom: Sau khi duplicate, sửa tags của bài mới — tags của bài gốc cũng đổi.

Constraint: Không dùng thư viện ngoài. Phải dùng spread.

Câu hỏi:

  1. Tại sao tags của bài gốc bị ảnh hưởng?
  2. Viết lại duplicatePost để tạo bản sao độc lập đến cấp meta.tags (không cần deep clone author).
  3. Nếu tags là array, tại sao [...post.meta.tags] an toàn hơn post.meta.tags?
[Đáp án tham khảo]
  • Bạn nghĩ:

    1. { ...post } chỉ shallow copy. meta vẫn là reference cũ. id: null chỉ ghi đè top-level.
    2. js
      function duplicatePost(post) {
        return {
          ...post,
          id: null,
          meta: {
            ...post.meta,
            tags: [...post.meta.tags]
          }
        };
      }
    3. [...post.meta.tags] tạo array mới. Nếu gán tags: post.meta.tags, cả hai bài dùng chung array — push/pop sẽ ảnh hưởng lẫn nhau.
  • Bài học: "Duplicate" trong production thường yêu cầu shallow copy đến 2–3 cấp. Spread giúp bạn làm điều này explicit — nhưng cũng dễ quên một cấp nếu không cẩn thận.

13. AI-assisted Exercise (Bài tập với AI) ​

Level B — Challenge

  1. Tự trả lời trước: Viết đoạn code dùng spread để merge object. Dự đoán kết quả:
    js
    const a = { x: { value: 1 } };
    const b = { ...a };
    b.x.value = 2;
    console.log(a.x.value);
  2. Hỏi AI: "Tại sao a.x.value cũng thành 2? Tôi đã dùng spread để copy mà?"
  3. So sánh câu trả lời AI với nhận định của bạn. AI có nhấn mạnh rằng spread chỉ shallow copy và giải thích tại sao x vẫn là reference cũ?
  4. Verify bằng MDN: tìm "Spread syntax" trên MDN, đọc phần "Shallow copy".

Gợi ý

AI thường biết câu trả lời đúng nhưng đôi khi dùng từ mơ hồ như "spread creates a new object" mà không nói rõ nested properties are not cloned. Nếu AI không chỉ ra rằng b.x === a.x là true, bạn đã tìm ra điểm mù. AI cũng có thể nhầm lẫn với structuredClone (deep clone) nếu bạn không specify.

[Đáp án tham khảo]
  • Bạn nghĩ: a.x.value là 2 vì spread chỉ copy reference của x. b.x và a.x trỏ cùng một object. Đây là shallow copy.

  • AI trả lời (typical):

    • Spread syntax creates a shallow copy of the object.
    • Top-level properties are copied by value (for primitives) or by reference (for objects).
    • Since x is an object, both a and b share the same reference to { value: 1 }.
    • Modifying b.x.value affects a.x.value.
    • The result is 2.
  • So sánh: AI thường đúng về behavior nhưng đôi khi không giải thích tại sao điều này quan trọng trong production: nếu bạn dùng spread để "clone" state trong React Redux/Zustand và quên nested spread, bạn sẽ tạo ra mutation bug mà React không detect được (vì reference thay đổi ở top-level nhưng nested không đổi reference).

  • Điểm AI nói sai hoặc quá mơ hồ: "Top-level properties are copied by value" — đúng với primitive nhưng gây hiểu nhầm với object. AI hiếm khi nói rõ: "Nếu bạn cần deep clone, spread không đủ. Bạn cần nested spread hoặc structuredClone."

  • Kết luận: Nếu bạn chỉ ra được rằng spread là surface-level duplication, không phải recursive duplication, bạn đã hiểu sâu hơn AI. Concept này sẽ quay lại ở Stage 2 (Object Model — prototype chain và property descriptors), Stage 8 (React immutable state), và Stage 11 (Memory — shared references).

14. Teach Back (Dạy lại) ​

Giả sử một junior developer hỏi bạn:

"Anh/chị ơi, em thấy người ta bảo dùng spread để copy object cho an toàn. Nhưng hôm nay em spread một object có settings bên trong, sửa settings.theme, và object gốc cũng đổi. Spread không copy à?"

Hãy giải thích trong 2 phút, dùng đúng terminology: shallow copy, reference, nested spread, immutable update.

Mô phỏng
  • Bạn nói: Spread có copy, nhưng là shallow copy — copy bề mặt.

Khi bạn viết { ...obj }, JavaScript tạo object mới và copy từng property từ obj sang. Nếu property là primitive như string, number — nó copy giá trị thật. Nhưng nếu property là object như settings — nó copy reference, tức là địa chỉ trỏ đến object cũ.

Vì vậy newObj.settings và obj.settings vẫn là cùng một object. Sửa newObj.settings.theme = sửa obj.settings.theme.

Để fix, bạn phải spread từng cấp:

js
{ ...obj, settings: { ...obj.settings, theme: "dark" } }

Điều này tạo settings mới, copy các property cũ, rồi ghi đè theme.

💡 Tưởng tượng spread như chụp ảnh bề mặt một tủ đồ. Ảnh mới có vẻ giống hệt, nhưng nếu trong tủ có một hộp nhỏ, cả hai ảnh đều trỏ đến cùng một hộp nhỏ đó. Muốn hoàn toàn độc lập, phải chụp lại cả hộp nhỏ.

Gợi ý đánh giá bản thân

  • Đồng nghiệp có hiểu tại sao spread không phải "deep copy" không?
  • Bạn có tránh được việc nói "spread copy hết" không?
  • Nếu đồng nghiệp hỏi: "Vậy khi nào em cần deep clone?" — bạn trả lời được không? (Gợi ý: khi nested depth không xác định, hoặc khi object chứa circular reference — dùng structuredClone hoặc library.)
  • Nếu đồng nghiệp viết { ...a, ...b } và b có property lạ lọt vào — bạn giải thích được trade-off không?

15. Assessment (Đánh giá) ​

Objective (Mục tiêu)Hình thức đánh giáTask
Object spread (Spread object)Implementation (Thực hành)Implementation Lab Level 1
Array spread (Spread array)Implementation (Thực hành)Implementation Lab Level 3
Immutable update (Cập nhật immutable)Implementation (Thực hành)Implementation Lab Level 2–3
Merge behavior (Gộp object)Prediction (Dự đoán)Prediction Câu 3, 5
Shallow vs deep (Nông vs sâu)Prediction (Dự đoán)Prediction Câu 2
Nested mutation bug (Bug mutation lồng nhau)Debug LabDebug Lab Section
Explain shallow copy limitTeach BackTeach Back Section
Distinguish merge safetyDesign ExerciseDesign Exercise Section

16. Exit Criteria (Tiêu chí qua bài) ​

  • [ ] Có thể sử dụng object spread để tạo shallow copy và merge object.
  • [ ] Có thể sử dụng array spread để tạo shallow copy và nối array.
  • [ ] Có thể kết hợp spread với property override để update immutably.
  • [ ] Có thể sử dụng spread arguments (fn(...args)) để truyền array làm argument list.
  • [ ] Có thể giải thích tại sao spread không phải deep copy.
  • [ ] Có thể nhận diện mutation bug khi spread object có nested object/array.
  • [ ] Có thể viết nested spread để immutable update đến 2 cấp.
  • [ ] Có thể debug lỗi "copied object still mutates original" do shallow reference.

17. Spiral Connection (Liên kết xoắn ốc) ​

Previous (Trước): Destructuring (0.6.7) — bạn đã biết cách "mở" object/array để lấy dữ liệu. Giờ bạn học cách "chiếu/sao chép" object/array để tạo mới từ cũ.

Current (Hiện tại): Spread & Shallow Copy — tạo container mới, copy surface-level properties, ghi đè có chọn lọc. Giới hạn: nested reference vẫn chia sẻ.

Next (Tiếp theo):

  • Stage 1 (Execution Model) — Reference value: tại sao b = a rồi sửa b lại ảnh hưởng a? Spread tạo object mới trong memory như thế nào? Thuộc về execution context và memory model.
  • Stage 2 (Object Model) — Property descriptors, Object.defineProperty, và Object.getOwnPropertyDescriptors. Tại sao spread chỉ copy enumerable own properties? Đây là giới hạn chính xác của spread semantics.
  • Stage 6 (TypeScript) — Spread types: { ...A, ...B } tạo intersection type gì? TypeScript bắt lỗi override type như thế nào?
  • Stage 8 (React) — State immutability: setState({ ...state, count: state.count + 1 }). React re-render dựa trên reference comparison. Shallow copy đúng cách là prerequisite cho React performance.
  • Stage 11 (Performance & Memory) — Deep clone cost, structuredClone, memory snapshot, và retained object. Khi nào shallow copy đủ, khi nào cần deep clone?
📴 Offline Mode — Content served from cache