Lesson 0.6.2 — Arrays
0. Metadata
| Field | Value |
|---|---|
| Stage | 0 — JavaScript Language Foundation |
| Module | 0.6 — Data Structures |
| Lesson | 0.6.2 |
| Competency | C01.6 — Arrays / Objects |
| Depth Target | L2–L3 |
| Prerequisites | Strings (0.6.1), Primitive vs Object (0.2.1/0.2.4), Variables & Bindings (0.2.2), typeof (0.2.5) |
| Estimated Cognitive Load | Medium |
1. Why This Exists (Vì sao cần học)
Bạn vừa học string: một chuỗi ký tự immutable — đọc được từng phần, nhưng thay đổi thì phải tạo mới. Giờ bạn gặp array: cũng là một dãy có thứ tự, nhưng hoàn toàn khác về bản chất.
Xét đoạn code:
function addToCart(cart, item) {
cart.push(item);
return cart;
}
const myCart = ["apple"];
const result = addToCart(myCart, "banana");
console.log(result); // ['apple', 'banana']
console.log(myCart); // ['apple', 'banana']
console.log(result === myCart); // trueVấn đề cốt lõi
addToCart vừa "thêm" vừa "phá". Nó trả về cart, nhưng myCart gốc cũng bị thay đổi. Nếu một đoạn code khác đang dùng myCart (ví dụ: hiển thị số lượng trước khi thêm), dữ liệu của họ bị ảnh hưởng silently.
Bài này xây mental model về array: mutable, ordered collection. Bạn phải biết method nào sửa array gốc, method nào tạo array mới, và tại sao const không bảo vệ array khỏi mutation.
2. Prerequisites (Yêu cầu đầu vào)
Trước khi học bài này, bạn cần:
- Hiểu string là immutable primitive, method trả về chuỗi mới (0.6.1).
- Phân biệt primitive và object: array là object value (0.2.1, 0.2.4).
- Biết
constcấm reassignment nhưng không cấm mutation (0.2.2). - Biết
typeof [] === "object"(0.2.5).
WARNING
Nếu bạn chưa chắc tại sao const arr = [1]; arr.push(2) hợp lệ, quay lại 0.2.2. Sự khác biệt giữa reassignment và mutation là trung tâm của bài này.
3. Learning Objectives (Mục tiêu học tập)
Sau bài này, bạn có thể:
- Tạo và truy cập array bằng literal và index.
- Phân biệt mutating methods (
push,pop,shift,unshift,splice) và non-mutating (slice, spread). - Dự đoán giá trị trả về của mutating methods (ví dụ:
pushtrả về length, không phải array). - Sử dụng
lengthđể đọc và hiểu giới hạn của nó. - Viết function xử lý array mà không mutate input ngoài ý muốn.
- Giải thích tại sao
constkhông ngăn đượcpush.
4. Mental Model (Mô hình tư duy)
Mental Model
String (Immutable) Array (Mutable)
┌──────────┐ ┌─────────┐
│ H e l l o│ │ a b c d │
└──────────┘ └─────────┘
Đọc: str[0] Đọc: arr[0]
Ghi: str[0] = "X" ❌ Ghi: arr[0] = "X" ✅
Thêm: str + "!" → new Thêm: arr.push("e") → mutate
Xóa: slice → new Xóa: arr.pop() → mutate
Array = Mutable Sequence
Index: 0 1 2 3
Values: [A] [B] [C] [D]
↑ ↑
arr[0] arr.length - 1
Mutating Method (Sửa gốc)
push(item) → thêm cuối, trả về new length
pop() → xóa cuối, trả về phần tử bị xóa
shift() → xóa đầu, trả về phần tử bị xóa
unshift(item) → thêm đầu, trả về new length
splice(...) → xóa/thay/thêm giữa, trả về mảng bị xóa
Non-Mutating Method (Tạo mới)
slice(start, end) → cắt ra mảng con mới
[...arr] → spread tạo bản sao nôngQuy tắc vàng: Mutating method vừa sửa array gốc, vừa trả về thứ không phải array mới. Nếu bạn gán kết quả của push vào biến, bạn vừa mutate gốc vừa nhận được một con số (length) — đây là lỗi cực kỳ phổ biến.
const + Array: const khóa biến (không cho gán array khác), nhưng không khóa nội dung bên trong array. Array là object, mutation là thay đổi nội bộ, không phải reassignment.
5. Core Concepts (Các khái niệm cốt lõi)
Essential (Bắt buộc)
| Concept | Ý nghĩa |
|---|---|
| Array Literal | [1, 2, 3] — tạo array object. |
| Index Access | arr[0] đọc phần tử tại index 0. arr[i] = x ghi đè tại index. |
| Length | arr.length — số phần tử. Tự động cập nhật khi push/pop. |
push(item) | Thêm vào cuối. Mutate. Trả về length mới. |
pop() | Xóa cuối. Mutate. Trả về phần tử bị xóa (hoặc undefined). |
shift() | Xóa đầu. Mutate. Trả về phần tử bị xóa. |
unshift(item) | Thêm vào đầu. Mutate. Trả về length mới. |
splice(start, deleteCount, ...items) | Xóa/thay/thêm tại vị trí bất kỳ. Mutate. Trả về mảng các phần tử bị xóa. |
slice(start, end) | Cắt mảng con. Non-mutate. Trả về mảng mới. |
Spread ([...arr]) | Tạo bản sao nông (shallow copy) của array. |
Supporting (Hỗ trợ)
| Concept | Ý nghĩa |
|---|---|
Array.isArray(value) | Kiểm tra một value có phải array không. typeof [] === "object" nên cần hàm riêng. |
indexOf(search) | Trả về index đầu tiên tìm thấy search, hoặc -1 nếu không có. Dùng để kiểm tra sự tồn tại của phần tử. |
| Sparse Array | [1, , 3] có "lỗ trống". length vẫn đếm, nhưng arr[1] là undefined. |
Awareness (Biết tồn tại)
| Concept | Lý do chưa đào sâu |
|---|---|
Array.prototype methods nâng cao | map, filter, reduce, find, some, every — học ở 0.6.3–0.6.4. |
| Typed Arrays | Uint8Array, Float32Array — dùng cho binary data, học ở Stage 11. |
Out of Scope (Không thuộc bài này)
- Iteration methods (
forEach,map,filter) — 0.6.3. reduce— 0.6.4.- Deep clone — 0.6.8.
- Array-like objects (
arguments,NodeList) — Stage 2. Array.from,Array.of— Stage 2.
6. Worked Example (Ví dụ phân tích từng bước)
Bài toán: Quản lý hàng đợi task (FIFO — First In First Out).
const queue = ["task-a", "task-b", "task-c"];Yêu cầu:
- Thêm task mới vào cuối.
- Lấy task tiếp theo ra xử lý.
- Kiểm tra queue sau mỗi thao tác.
function processNext(queue) {
const next = queue.shift();
queue.push("cleanup");
return next;
}
const q = ["email", "backup"];
const current = processNext(q);
console.log(current); // "email"
console.log(q); // ["backup", "cleanup"] ❓function peekNext(queue) {
return queue[0];
}
function dequeue(queue) {
const next = queue[0];
const remaining = queue.slice(1);
return { next, remaining };
}
function enqueue(queue, task) {
return [...queue, task];
}
const q = ["email", "backup"];
const { next, remaining } = dequeue(q);
const updated = enqueue(remaining, "cleanup");
console.log(next); // "email"
console.log(q); // ["email", "backup"] ✅ không đổi
console.log(updated); // ["backup", "cleanup"]Walkthrough
Step 1 — Phân tích BeforeprocessNext làm 3 việc:
shift()— xóa"email"khỏiqgốc, trả về"email".push("cleanup")— thêm vàoqgốc, trả về2(length), bị bỏ qua.return next.
Kết quả: q bị mutate từ ["email", "backup"] thành ["backup", "cleanup"]. Nếu một component khác đang render q trước khi gọi processNext, nó sẽ thấy dữ liệu "nhảy" không rõ nguyên nhân.
Step 2 — Dequeue không mutatedequeue dùng slice(1) để tạo mảng mới từ index 1 đến hết. q gốc không đổi. Trả về object chứa cả phần tử lấy ra và mảng còn lại.
Step 3 — Enqueue không mutateenqueue dùng spread [...queue, task] để tạo mảng mới. Không đụng đến queue gốc.
Step 4 — Caller kiểm soát mutationq luôn giữ nguyên. Muốn "tiến hóa" state, caller tự gán updated. Đây là pattern sẽ gặp lại ở React (Stage 8): không mutate state, thay thế bằng state mới.
Key Insight: shift, push, pop tiện lợi nhưng nguy hiểm vì mutate. Khi bạn cần predictability (biết chắc input không đổi), dùng slice và spread.
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 arr = [10, 20];
const result = arr.push(30);
console.log(result);
console.log(arr);Câu 2
const arr = ["a", "b", "c"];
const x = arr.pop();
const y = arr.shift();
console.log(x, y, arr);Câu 3
const arr = [1, 2, 3, 4];
const sliced = arr.slice(1, 3);
console.log(sliced);
console.log(arr);Câu 4
const a = [1, 2];
const b = a;
b.push(3);
console.log(a);
console.log(a === b);Câu 5
const arr = ["x", "y", "z"];
const removed = arr.splice(1, 1);
console.log(removed);
console.log(arr);[Đáp án & Giải thích]
Câu 1: 3 và [10, 20, 30] — push trả về new length
- Giải thích:
push(30)thêm30vào cuốiarr(mutate), rồi trả vềlengthmới là3.arrbây giờ là[10, 20, 30]. Lỗi phổ biến: tưởngresultlà array mới, hoặc nhầmpushtrả về length cũ.
Câu 2: "c", "a", ["b"] — pop + shift
- Giải thích:
pop()xóa cuối ("c"), trả về"c".shift()xóa đầu ("a"), trả về"a".arrcòn lại["b"]. Cả hai đều mutate.
Câu 3: [2, 3] và [1, 2, 3, 4] — slice không mutate
- Giải thích:
slice(1, 3)cắt từ index 1 đến trước index 3 →[2, 3].arrgốc không đổi. Đây là non-mutating method.
Câu 4: [1, 2, 3] và true — Reference sharing
- Giải thích:
b = akhông copy mảng, chỉ copy reference.b.push(3)mutate cùng một object màatrỏ đến.a === bvì cùng reference.
Câu 5: ["y"] và ["x", "z"] — splice trả về removed
- Giải thích:
splice(1, 1)bắt đầu tại index 1, xóa 1 phần tử. Phần tử bị xóa là"y", được trả về trong mảng["y"].arrgốc bị sửa thành["x", "z"].
8. Implementation Lab (Bài lab thực hành)
Level 1 — Guided (Có hướng dẫn)
Viết lại function removeLast để không mutate input array. Trả về object chứa removed (phần tử bị xóa) và remaining (mảng mới).
function removeLast(items) {
// 1. Lấy phần tử cuối bằng index
// 2. Dùng slice để tạo mảng mới không có phần tử cuối
// 3. Trả về { removed, remaining }
}
const tasks = ["a", "b", "c"];
const result = removeLast(tasks);
console.log(result);
// { removed: "c", remaining: ["a", "b"] }
console.log(tasks);
// ["a", "b", "c"] ✅ không đổiGợi ý
items[items.length - 1] đọc phần tử cuối. items.slice(0, -1) cắt từ đầu đến trước phần tử cuối.
[Đáp án tham khảo]
function removeLast(items) {
const removed = items[items.length - 1];
const remaining = items.slice(0, -1);
return { removed, remaining };
}
const tasks = ["a", "b", "c"];
const result = removeLast(tasks);
console.log(result);
// { removed: "c", remaining: ["a", "b"] }
console.log(tasks);
// ["a", "b", "c"]Giải thích:
items.length - 1là index cuối cùng của array.items.slice(0, -1)dùng index âm:-1nghĩa là "trước phần tử cuối", nên cắt từ 0 đến trước cuối.slicekhông mutateitems.tasksgốc giữ nguyên.
Level 2 — Partial Scaffold (Khung sẵn)
Hoàn thành function insertAt để chèn một phần tử vào vị trí bất kỳ mà không mutate array gốc.
function insertAt(items, index, item) {
// 1. Lấy phần trước index
const before = items._______(0, index);
// 2. Lấy phần từ index trở đi
const after = items._______(index);
// 3. Nối ba phần: before + [item] + after
return [..._______, item, ..._______];
}
console.log(insertAt(["a", "b", "c"], 1, "X"));
// ["a", "X", "b", "c"]
console.log(insertAt(["a", "b"], 0, "Z"));
// ["Z", "a", "b"][Đáp án tham khảo]
function insertAt(items, index, item) {
const before = items.slice(0, index);
const after = items.slice(index);
return [...before, item, ...after];
}
console.log(insertAt(["a", "b", "c"], 1, "X"));
// ["a", "X", "b", "c"]
console.log(insertAt(["a", "b"], 0, "Z"));
// ["Z", "a", "b"]Giải thích:
slice(0, index)lấy phần tử từ đầu đến trướcindex.slice(index)lấy phần tử từindexđến hết.- Spread
...beforevà...after"trải" các phần tử ra, chènitemở giữa. - Không dùng
splicevìsplicemutate array gốc.
Level 3 — Independent (Tự viết)
Viết các function xử lý array sau (không dùng thư library, không mutate input):
// 1. swapFirstLast(items) → Đổi chỗ phần tử đầu và cuối, trả về mảng mới
// Ví dụ: swapFirstLast([1, 2, 3]) → [3, 2, 1]
// 2. withoutItem(items, index) → Trả về mảng mới không có phần tử tại index
// Ví dụ: withoutItem(["a", "b", "c"], 1) → ["a", "c"]
// 3. flattenOneLevel(nested) → Trả về mảng "dẹt" một cấp
// Ví dụ: flattenOneLevel([[1, 2], [3], [4, 5]]) → [1, 2, 3, 4, 5]
// Sử dụng:
const nums = [1, 2, 3];
console.log(swapFirstLast(nums)); // [3, 2, 1]
console.log(nums); // [1, 2, 3] ✅ không đổi[Đáp án tham khảo]
function swapFirstLast(items) {
if (items.length < 2) return [...items];
const first = items[0];
const last = items[items.length - 1];
const middle = items.slice(1, -1);
return [last, ...middle, first];
}
function withoutItem(items, index) {
return [...items.slice(0, index), ...items.slice(index + 1)];
}
function flattenOneLevel(nested) {
// Giả định: nested là array chứa các array con
const result = [];
for (const arr of nested) {
for (const item of arr) {
result.push(item);
}
}
return result;
}
const nums = [1, 2, 3];
console.log(swapFirstLast(nums));
// [3, 2, 1]
console.log(nums);
// [1, 2, 3]
console.log(withoutItem(["a", "b", "c"], 1));
// ["a", "c"]
console.log(flattenOneLevel([[1, 2], [3], [4, 5]]));
// [1, 2, 3, 4, 5]Giải thích:
swapFirstLast: Lấy phần tử đầu, cuối, và middle bằngslice(1, -1). Nối lại bằng literal array mới.withoutItem: Dùngsliceđể lấy phần trước và sauindex, rồi spread nối lại.flattenOneLevel: Lặp nested array, push từng phần tử vàoresultmới. Không mutate input.- Tất cả function đều trả về dữ liệu mới, không sửa input.
9. Edge Cases (Các trường hợp ngoại lệ)
push trả về length, không phải array
const arr = [1, 2];
const result = arr.push(3);
console.log(result); // 3 ❌ (không phải [1, 2, 3])
console.log(arr); // [1, 2, 3]Tại sao: Nhiều junior tưởng push giống concat (trả về array mới). push mutate và trả về length.
Cách nhận biết: Biến nhận kết quả push là một số, gây lỗi .map hoặc .length tiếp theo.
Cách xử lý: Nếu cần array mới, dùng spread: [...arr, 3].
const không bảo vệ array khỏi mutation
const arr = [1];
arr.push(2); // ✅ hợp lệ
arr[0] = 99; // ✅ hợp lệ
arr = [3]; // ❌ TypeErrorTại sao: const khóa binding (biến arr không thể trỏ chỗ khác), nhưng array là object nên nội dung vẫn sửa được.
Cách nhận biết: Dùng const nhưng array vẫn "tự đổi" sau khi qua function.
Cách xử lý: Nếu muốn bảo vệ hoàn toàn, cần Object.freeze(arr) (Stage 2) hoặc không dùng mutating methods.
splice trả về mảng bị xóa, không phải mảng mới sau khi sửa
const arr = ["a", "b", "c"];
const result = arr.splice(1, 1, "X");
console.log(result); // ["b"] ← mảng bị xóa
console.log(arr); // ["a", "X", "c"]Tại sao: splice vừa mutate gốc, vừa trả về "những gì bị lấy ra". Đây là dual-purpose method dễ gây nhầm.
Cách nhận biết: result là mảng các phần tử bị xóa, không phải array sau modification.
Cách xử lý: Nếu cần array sau khi chèn/xóa, đọc arr sau khi splice, hoặc dùng slice + spread để không mutate.
Sparse array — length đếm cả "lỗ trống"
const arr = [1, , 3];
console.log(arr.length); // 3
console.log(arr[1]); // undefinedTại sao: Array có thể có "empty slots". length vẫn là 3 vì index cao nhất là 2.
Cách nhận biết: Loop hoặc map bỏ qua empty slot (behavior khác undefined thật sự).
Cách xử lý: Tránh tạo sparse array. Nếu cần placeholder, dùng null hoặc undefined explicit: [1, null, 3].
slice với index vượt quá length
const arr = [1, 2];
console.log(arr.slice(0, 10)); // [1, 2]
console.log(arr.slice(5, 10)); // []Tại sao: slice không throw khi index vượt quá. Nó trả về những gì có thể cắt được.
Cách nhận biết: Không phải bug, nhưng cần biết để không defensive-check thừa.
Cách xử lý: slice an toàn để dùng mà không cần kiểm tra bounds.
10. Debug Lab (Bài lab gỡ lỗi)
Symptom (Triệu chứng): Function clearCart được gọi để "làm trống giỏ hàng trước khi thanh toán", nhưng sau khi gọi, giỏ hàng gốc vẫn còn nguyên.
function clearCart(cart) {
cart = [];
return cart;
}
const myCart = ["apple", "banana"];
clearCart(myCart);
console.log(myCart); // ["apple", "banana"] ❌Reproduction (Tái hiện lỗi): Chạy code trên.
Evidence (Bằng chứng):
myCartvẫn là["apple", "banana"]sau khi gọiclearCart.- Không có lỗi throw.
Hypothesis (Giả thuyết): cart = [] bên trong clearCart chỉ reassign biến local cart. Nó không affect myCart bên ngoài vì:
cartnhận copy của reference (trỏ đến cùng object).cart = []tạo array mới và gáncarttrỏ sang đó.myCartvẫn trỏ đến array cũ.
Verification (Xác minh):
function clearCart(cart) {
console.log(cart === myCart); // true (cùng object)
cart = [];
console.log(cart === myCart); // false (đã trỏ chỗ khác)
}Root Cause (Nguyên nhân gốc rễ): Nhầm lẫn giữa reassignment (cart = []) và mutation (cart.length = 0 hoặc cart.pop() loop). Reassignment chỉ ảnh hưởng biến local. Để "làm trống" array gốc, phải mutate nội dung hoặc caller tự gán kết quả mới.
Fix (Sửa) — Option 1: Mutate (nếu intent là clear thật)
function clearCart(cart) {
cart.length = 0;
return cart;
}Fix (Sửa) — Option 2: Return new array (không mutate)
function clearCart(cart) {
return [];
}
const myCart = ["apple", "banana"];
const emptyCart = clearCart(myCart);
// myCart vẫn còn, emptyCart là mớiPrevention (Phòng ngừa):
- Nếu function cần "sửa" array gốc, dùng mutating method (
length = 0,splice, v.v.) và đặt tên rõ ràng:emptyCartInPlace. - Nếu function trả về array mới, đặt tên như
createEmptyCarthoặcclearCartCopy, và caller phải gán kết quả. - Trong code review, nghi ngờ mọi
param = []bên trong function — nó không affect caller ( Tự đặt câu hỏi : "Có phải dev đang tưởng rằng nó sẽ thay đổi array bên ngoài không?")
11. Design Exercise (Bài tập thiết kế giải pháp)
Bạn cần viết một hàm thêm sản phẩm vào lịch sử xem gần đây (recently viewed). Danh sách tối đa 5 sản phẩm, không trùng lặp.
function addToRecent(recent, productId) {
const index = recent.indexOf(productId);
if (index !== -1) {
recent.splice(index, 1);
}
recent.unshift(productId);
if (recent.length > 5) {
recent.pop();
}
return recent;
}// Lưu ý : Phần này chưa dùng tới `filter` trong ví dụ vì out of scope
// Thay `filter` (out of scope) bằng `indexOf` + `slice` + spread
function addToRecent(recent, productId) {
const index = recent.indexOf(productId);
const filtered = index === -1
? [...recent]
: [...recent.slice(0, index), ...recent.slice(index + 1)];
const updated = [productId, ...filtered];
return updated.slice(0, 5);
}Câu hỏi:
- Option A có lợi gì về performance?
- Option B có lợi gì về predictability?
- Nếu
recentđược dùng ở 3 component khác nhau trong UI, option nào an toàn hơn? Tại sao?
[Đáp án tham khảo]
Bạn nghĩ:
- Option A không tạo array mới (trừ khi cần splice), tiết kiệm bộ nhớ tạm thời.
- Option B:
recentgốc không bao giờ đổi. Các component khác đang referencerecentsẽ không bị ảnh hưởng. Dễ test: cho input, so sánh output. - Option B an toàn hơn. Nếu Component A đang render
recent, Component B gọiaddToRecent, Component A sẽ không bị re-render bất ngờ (với React) hoặc dữ liệu không bị "nhảy" (với vanilla JS). Option A mutate shared reference, gây coupling ẩn (sự phụ thuộc không rõ ràng) giữa các component.
Kết luận: Với data được share giữa nhiều nơi, non-mutating approach (Option B) giảm bug silent. Performance khác biệt ở scale nhỏ (5 phần tử) là không đáng kể. Đây là pattern sẽ gặp lại ở React state management.
12. Production Scenario (Tình huống thực tế)
Context: Một dashboard hiển thị danh sách notification. Mỗi khi có notification mới, addNotification được gọi:
const notifications = [];
function addNotification(list, message) {
list.push({ id: Date.now(), message });
if (list.length > 10) {
list.shift();
}
return list;
}Symptom: Một component "Notification Count" hiển thị số notifications.length. Nhưng đôi khi count giảm xuống 0 rồi tăng lên 1, dù không có action xóa nào từ user. Hoặc một component history log thấy notification biến mất khỏi giữa danh sách.
Constraint: notifications là global array được import từ module khác. Nhiều component subscribe vào nó.
Câu hỏi:
- Tại sao notification "biến mất" khỏi giữa danh sách?
addNotificationlà pure hay impure? Tại sao?- Viết lại
addNotificationđể không mutatenotificationsgốc.
[Đáp án tham khảo]
Bạn nghĩ:
shift()xóa phần tử đầu danh sách khilength > 10. Nếu user đang xem notification ở giữa hoặc cuối, phần tử đầu (cũ nhất) bị xóa. Component history log thấy mất dữ liệu. Count "nhảy" vìshift()đồng bộ thay đổilength.- Impure.
pushvàshiftđều mutatelist(vànotificationsgốc vì cùng reference). Có side effect observable bên ngoài. - js
function addNotification(list, message) { const updated = [ { id: Date.now(), message }, ...list ]; return updated.slice(0, 10); } // Caller: const notifications = []; // ... const next = addNotification(notifications, "New alert"); // notifications giữ nguyên, next là mới
Bài học: Mutate shared array = race condition ngay cả trong single-threaded JavaScript. Một function thay đổi data mà component khác đang dùng là bug "spooky action at a distance" - "Sửa A, nhưng B tự nhiên hỏng.".
13. AI-assisted Exercise (Bài tập với AI)
Level B — Challenge
- Tự trả lời trước: Viết một đoạn code mà AI có thể nghĩ là "tạo bản sao array" nhưng thực ra vẫn mutate gốc. Gợi ý: dùng
slicenhưng sau đó mutate phần tử bên trong.jsconst items = [{ name: "A" }, { name: "B" }]; const copy = items.slice(); copy[0].name = "Z"; console.log(items[0].name); - Hỏi AI: "Đoạn code này có mutate
itemsgốc không? Tại sao?" - So sánh câu trả lời AI với nhận định của bạn. AI có nhắc đến shallow copy không?
- Verify bằng MDN: tìm
Array.prototype.slicetrên MDN và đọc phần "Description", tìm từ "shallow".
Gợi ý
AI thường trả lời đúng rằng items[0].name thành "Z", nhưng đôi khi giải thích mơ hồ bằng cách nói "slice tạo bản sao nhưng object bên trong vẫn shared". Nếu AI không dùng từ shallow copy hoặc không giải thích rằng slice chỉ copy reference của phần tử (không copy object sâu), bạn đã tìm ra điểm mù. Đây là lỗi production thực tế.
Đáp án tham khảo
Bạn nghĩ:
itemsbị mutate.slice()tạo array mới (shallow copy), nhưng phần tử[0]vẫn trỏ đến cùng object{ name: "A" }. Sửa.namequacopy[0]cũng sửa object gốc. Đây là shallow copy bug.AI trả lời (typical):
slicecreates a shallow copy of the array.- The objects inside are still referenced, not duplicated.
- So
copy[0].name = "Z"mutates the original object. items[0].namewill be"Z".
So sánh: AI thường nhận ra shallow copy nhưng đôi khi diễn đạt như thể đây là "exception" của
slice. AI hiếm khi nhấn mạnh rằng: đây không phải bug củaslice— đây là behavior đúng theo spec. Shallow copy là intentional, không phải limitation.Điểm AI nói sai hoặc quá mơ hồ: "The objects inside are still referenced" — đúng nhưng chưa đủ. AI không giải thích tại sao điều này quan trọng trong production: nếu
itemslà React state,copy[0].name = "Z"vừa mutate state trực tiếp (vi phạm immutability pattern) vừa có thể không trigger re-render.Kết luận: Nếu bạn chỉ ra được rằng
slice+ mutate phần tử object = silent mutation bug không khác gìpush, bạn đã hiểu sâu hơn AI. Concept này sẽ quay lại ở 0.6.8 (Shallow Copy), Stage 8 (React state immutability), và Stage 11 (Memory/Performance).
14. Teach Back (Dạy lại)
Giả sử một junior developer hỏi bạn:
"Em thấy
const arr = [1, 2]; arr.push(3)vẫn chạy được. Vậyconstcó ý nghĩa gì? Và tại sao người ta bảo array 'mutable' trong khi string thì 'immutable'? Cả hai đều là dãy mà?"
Hãy giải thích trong 2 phút, dùng đúng terminology: primitive, object, reference, mutation, reassignment.
Mô phỏng
- Bạn nói:
constkhóa biến, không khóa nội dung. Với primitive như string, nội dung không sửa được ngay từ đầu — đó là immutable. Với array, array là object, nên nội dung bên trong (các phần tử) sửa được — đó là mutable.
Ví dụ: const x = "hello" thì x luôn trỏ đến chuỗi "hello". Bạn không thể sửa chữ h thành H vì string không cho phép. Nhưng const arr = [1, 2] thì arr vẫn trỏ đến cùng một object array, và object đó cho phép bạn push(3) — nội dung thay đổi nhưng biến arr vẫn trỏ chỗ cũ.
So sánh: String giống tờ giấy đã in — bạn không sửa được chữ trên đó, chỉ có thể photo ra tờ mới. Array giống tủ hồ sơ — bạn không đổi tủ khác, nhưng vẫn thêm/xóa hồ sơ bên trong.
💡 Tưởng tượng
constnhư một cái bảng tên trên cửa. Cửa phòng string thì khóa kín — bạn không sửa gì bên trong được. Cửa phòng array thì mở — bạn vào thêm đồ được, nhưng bảng tên trên cửa (const arr) thì không được thay.
Gợi ý đánh giá bản thân
- Đồng nghiệp có hiểu tại sao
const+pushhợp lệ không? - Bạn có tránh được việc nói "array là mutable nên dùng
let" không? (constvẫn đúng cho array nếu không reassign.) - Nếu đồng nghiệp hỏi: "Vậy làm sao để array 'không đổi'?" — bạn trả lời được không? (Gợi ý: dùng
slice/spreadtạo mới, hoặcObject.freeze.) - Nếu đồng nghiệp hỏi:
const copy = arr.slice(); copy[0].name = "Z"có mutatearrkhông? — bạn giải thích được shallow copy trong 30 giây không?
15. Assessment (Đánh giá)
| Objective (Mục tiêu) | Hình thức đánh giá | Task |
|---|---|---|
| Create and access array (Tạo và truy cập array) | Implementation (Thực hành) | Implementation Lab Level 1 |
| Distinguish mutating vs non-mutating (Phân biệt mutate/non-mutate) | Prediction (Dự đoán) | Prediction Exercise Câu 1–3 |
| Predict return value of mutating methods (Dự đoán giá trị trả về) | Prediction (Dự đoán) | Prediction Exercise Câu 1, 5 |
Use length correctly (Dùng length đúng) | Implementation (Thực hành) | Implementation Lab Level 2 |
| Write function without mutating input (Viết function không mutate input) | Implementation (Thực hành) | Implementation Lab Level 3 |
Explain why const doesn't prevent mutation (Giải thích const và mutation) | Teach Back | Teach Back Section |
| Debug reassignment vs mutation confusion (Gỡ lỗi nhầm lẫn reassign/mutate) | Debug Lab | Debug Lab Section |
| Choose mutating vs non-mutating approach (Chọn approach mutate/non-mutate) | Design Decision | Design Exercise |
16. Exit Criteria (Tiêu chí qua bài)
- [ ] Có thể tạo array bằng literal và truy cập phần tử bằng index.
- [ ] Có thể phân biệt 5 mutating methods (
push,pop,shift,unshift,splice) và 2 non-mutating methods (slice, spread) về behavior và return value. - [ ] Có thể dự đoán giá trị trả về của
push(length),pop/shift(removed element),splice(array of removed). - [ ] Có thể giải thích tại sao
const arr = []vẫn cho phéparr.push()(const khóa binding, không khóa mutation). - [ ] Có thể viết function xử lý array bằng
slicevà spread, trả về array mới, không mutate input. - [ ] Có thể debug bug "function không affect array gốc" do reassignment thay vì mutation.
- [ ] Có thể nhận diện shallow copy behavior khi array chứa object references.
- [ ] Có thể chọn giữa mutate-in-place và return-new-array dựa trên context (shared data vs local data).
17. Spiral Connection (Liên kết xoắn ốc)
Previous (Trước): Strings (0.6.1) — bạn đã biết sequence immutable. Giờ bạn học sequence mutable (array) và thấy sự khác biệt sâu sắc giữa primitive và object behavior.
Current (Hiện tại): Array Operations — creation, indexing, mutating vs non-mutating methods,
length, reference sharing. Đây là nền tảng cho mọi thao tác collection trong JavaScript.
Next (Tiếp theo):
- 0.6.3 (Array Iteration) —
forEach,map,filter,find: các method xử lý array theo functional style, không mutate (trừforEachcó thể mutate nếu callback làm vậy).- 0.6.4 (Reduce) — Tổng hợp array thành một giá trị duy nhất.
- 0.6.8 (Spread & Shallow Copy) — Đào sâu shallow copy vs deep copy, tại sao
slicevà spread không đủ với nested objects.- Stage 1 (Execution Model) — Closure + array = private state pattern. Array được "nhớ" bên trong closure.
- Stage 8 (React) — State là array/object → không bao giờ mutate trực tiếp, luôn tạo mới bằng spread.
- Stage 11 (Performance) — Mutating large arrays vs creating new arrays: trade-off giữa correctness và memory.