Lesson 0.6.8 — Spread & Shallow Copy
0. Metadata
| Field | Value |
|---|---|
| Stage | 0 — JavaScript Language Foundation |
| Module | 0.6 — Data Structures |
| Lesson | 0.6.8 |
| Competency | C01.6 — Arrays / Objects |
| Depth Target | L2–L3 |
| Prerequisites | Objects (0.6.5), Arrays (0.6.2), Destructuring (0.6.7) |
| Estimated Cognitive Load | Medium–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:
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
constbinding 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ể:
- Sử dụng object spread (
{ ...obj }) để tạo shallow copy của object. - Sử dụng array spread (
[ ...arr ]) để tạo shallow copy của array. - Kết hợp spread với property override để update object immutably.
- Sử dụng spread để merge nhiều object/array.
- Phân biệt shallow copy và deep copy ở mức khái niệm.
- Nhận diện mutation bug khi spread object có nested object.
- 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ốcQuy 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 Arguments | fn(...args) — trải array thành argument list. |
| Shallow Copy | Copy container mới, nhưng nested object vẫn share reference. |
Supporting (Hỗ trợ)
| Concept | Ý nghĩa |
|---|---|
| Object.assign | Object.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)
| Concept | Lý do chưa đào sâu |
|---|---|
| Deep Clone | Cầ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 protocol | Array 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.
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.
const updatedUser = user;
updatedUser.profile.role = "admin";
updatedUser.profile.theme = "dark";
// user cũng bị đổi ❌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ànconst 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ácWalkthrough
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.rolesẽ sửa luônuser.profile.rolevì 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).profilelà object nên copy by reference. - Reasoning: Spread syntax (
...) copy own enumerable properties từ source sang target. Vớiprofile, 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.profilevàuser.profilevẫ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
const a = { x: 1, y: 2 };
const b = { ...a };
b.x = 99;
console.log(a.x);Câu 2
const a = { nested: { value: 10 } };
const b = { ...a };
b.nested.value = 99;
console.log(a.nested.value);Câu 3
const defaults = { theme: "light", lang: "en" };
const user = { lang: "vi" };
const settings = { ...defaults, ...user };
console.log(settings.theme, settings.lang);Câu 4
const nums = [1, 2];
const more = [...nums, 3, ...nums];
console.log(more);Câu 5
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)
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, copyxvày.b.x = 99sửa property trên object mới.a.xkhông đổi.
Câu 2: 99 — Nested reference shared
- Giải thích:
{ ...a }copy propertynested. Nhưngnestedlà reference trỏ đến cùng một object{ value: 10 }.b.nested.value = 99sửa object được cảavàbchia sẻ. Shallow copy không bảo vệ nested mutation.
Câu 3: "light" "vi" — Merge + override
- Giải thích:
...defaultscopytheme: "light",lang: "en". Sau đó...userghi đèlang: "vi". Key sau thắng key trước.
Câu 4: [1, 2, 3, 1, 2] — Array spread + concat
- Giải thích:
...numstrải[1, 2].morelà[1, 2, 3, 1, 2]. Array gốc không đổi.
Câu 5: 1 3 — Override order
- Giải thích:
...bcopyx: 2, y: 3....acopyx: 1, ghi đèxtrước đó.ygiữ nguyên3. Thứ tự spread quyết định kết quả.
Câu 6: "light" true 14
- Giải thích:
...settingscopytheme: "dark",notifications: true....prefscopyfontSize: 14.theme: "light"ghi đèthemetrước đó.
Nếu đổi thứ tự:{ ...prefs, ...settings, theme: "light" }vẫn chotheme: "light"vì property sau cùng vẫn làtheme: "light", nhưng nếusettingscófontSizethì...settingssẽ 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.
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]
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);
// falseGiải thích:
{ ...todo }tạo object mới với toàn bộ property củatodo.completed: !todo.completedghi đè propertycompletedtrên object mới.todogố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.
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]
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:
...usercopy toàn bộ property top-level.profile: { ...user.profile, theme: newTheme }tạoprofilemới, copy role cũ, ghi đè theme.user.profilevẫn là object cũ, không đổi.
Level 3 — Independent (Tự viết)
Viết các function sau sử dụng spread:
// 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]
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:...defaultstrước,...overridessau. Key trùng bị ghi đè bởi overrides.addTag:...postcopy top-level.tags: [...post.tags, tag]tạo arraytagsmới từ array cũ + thêm phần tử.post.tagskhô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.
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]
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
const a = { inner: { value: 1 } };
const b = { ...a };
b.inner.value = 2;
console.log(a.inner.value); // 2Tạ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
const a = { x: 1 };
const b = { x: 2 };
const c = { ...a, ...b, x: 3 };
console.log(c.x); // 3Tạ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)
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)
const a = null;
const b = { ...a }; // ✅ {} — object spread cho phép null/undefined
const c = [...a]; // TypeError: a is not iterableTạ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):
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ưngprofilevẫn là reference cũ.copy.profile.role = newRolesửa objectprofileđược cảuservàcopychia sẻ.users[0].profile.roleđổi thành"admin"dù object gốcuserskhô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):
console.log(copy.profile === user.profile); // true → chứng minh cùng referenceRoot 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):
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.
function createConfig(base, overrides) {
return { ...base, ...overrides };
}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:
- Option A có lợi gì khi base có 20 property?
- Option B có lợi gì về type safety và validation?
- Nếu
overrideschứa property lạ không thuộcbase(ví dụhacker: true), option nào nguy hiểm hơn?
[Đáp án tham khảo]
Bạn nghĩ:
- 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.
- Option B: Explicit, dễ đọc, dễ thêm validation từng field.
??phân biệtnullvàundefined. Không cho phép property lạ lọt vào. - Option A nguy hiểm hơn:
...overridescopy 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.
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:
- Tại sao tags của bài gốc bị ảnh hưởng?
- Viết lại
duplicatePostđể tạo bản sao độc lập đến cấpmeta.tags(không cần deep cloneauthor). - Nếu
tagslà array, tại sao[...post.meta.tags]an toàn hơnpost.meta.tags?
[Đáp án tham khảo]
Bạn nghĩ:
{ ...post }chỉ shallow copy.metavẫn là reference cũ.id: nullchỉ ghi đè top-level.- js
function duplicatePost(post) { return { ...post, id: null, meta: { ...post.meta, tags: [...post.meta.tags] } }; } [...post.meta.tags]tạo array mới. Nếu gántags: 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
- 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); - Hỏi AI: "Tại sao
a.x.valuecũng thành 2? Tôi đã dùng spread để copy mà?" - 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
xvẫn là reference cũ? - 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.valuelà2vì spread chỉ copy reference củax.b.xvàa.xtrỏ 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
xis an object, bothaandbshare the same reference to{ value: 1 }. - Modifying
b.x.valueaffectsa.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ó
settingsbên trong, sửasettings.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:
{ ...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
structuredClonehoặc library.) - Nếu đồng nghiệp viết
{ ...a, ...b }vàbcó 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 Lab | Debug Lab Section |
| Explain shallow copy limit | Teach Back | Teach Back Section |
| Distinguish merge safety | Design Exercise | Design 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 = arồi sửablại ảnh hưởnga? 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?