Skip to content

Lesson 0.6.2 — Arrays ​

Bài 0.6.2 — Arrays
Creation, Access, Mutation & Non-mutating operations • 32 phút
0:00 / 0:00

0. Metadata ​

FieldValue
Stage0 — JavaScript Language Foundation
Module0.6 — Data Structures
Lesson0.6.2
CompetencyC01.6 — Arrays / Objects
Depth TargetL2–L3
PrerequisitesStrings (0.6.1), Primitive vs Object (0.2.1/0.2.4), Variables & Bindings (0.2.2), typeof (0.2.5)
Estimated Cognitive LoadMedium

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:

js
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); // true

Vấ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 const cấ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ể:

  1. Tạo và truy cập array bằng literal và index.
  2. Phân biệt mutating methods (push, pop, shift, unshift, splice) và non-mutating (slice, spread).
  3. Dự đoán giá trị trả về của mutating methods (ví dụ: push trả về length, không phải array).
  4. Sử dụng length để đọc và hiểu giới hạn của nó.
  5. Viết function xử lý array mà không mutate input ngoài ý muốn.
  6. Giải thích tại sao const không ngăn được push.

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ông

Quy 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 Accessarr[0] đọc phần tử tại index 0. arr[i] = x ghi đè tại index.
Lengtharr.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) ​

ConceptLý do chưa đào sâu
Array.prototype methods nâng caomap, filter, reduce, find, some, every — học ở 0.6.3–0.6.4.
Typed ArraysUint8Array, 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).

js
const queue = ["task-a", "task-b", "task-c"];

Yêu cầu:

  1. Thêm task mới vào cuối.
  2. Lấy task tiếp theo ra xử lý.
  3. Kiểm tra queue sau mỗi thao tác.
js
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"] ❓
js
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:

  1. shift() — xóa "email" khỏi q gốc, trả về "email".
  2. push("cleanup") — thêm vào q gốc, trả về 2 (length), bị bỏ qua.
  3. 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 ​

js
const arr = [10, 20];
const result = arr.push(30);
console.log(result);
console.log(arr);

Câu 2 ​

js
const arr = ["a", "b", "c"];
const x = arr.pop();
const y = arr.shift();
console.log(x, y, arr);

Câu 3 ​

js
const arr = [1, 2, 3, 4];
const sliced = arr.slice(1, 3);
console.log(sliced);
console.log(arr);

Câu 4 ​

js
const a = [1, 2];
const b = a;
b.push(3);
console.log(a);
console.log(a === b);

Câu 5 ​

js
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êm 30 vào cuối arr (mutate), rồi trả về length mới là 3. arr bây giờ là [10, 20, 30]. Lỗi phổ biến: tưởng result là array mới, hoặc nhầm push trả 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". arr cò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]. arr gốc không đổi. Đây là non-mutating method.

Câu 4: [1, 2, 3] và true — Reference sharing

  • Giải thích: b = a không copy mảng, chỉ copy reference. b.push(3) mutate cùng một object mà a trỏ đến. a === b vì 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"]. arr gố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).

js
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 đổi

Gợ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]
js
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 - 1 là index cuối cùng của array.
  • items.slice(0, -1) dùng index âm: -1 nghĩa là "trước phần tử cuối", nên cắt từ 0 đến trước cuối.
  • slice không mutate items.
  • tasks gố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.

js
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]
js
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ước index.
  • slice(index) lấy phần tử từ index đến hết.
  • Spread ...before và ...after "trải" các phần tử ra, chèn item ở giữa.
  • Không dùng splice vì splice mutate 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):

js
// 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]
js
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ằng slice(1, -1). Nối lại bằng literal array mới.
  • withoutItem: Dùng slice để lấy phần trước và sau index, rồi spread nối lại.
  • flattenOneLevel: Lặp nested array, push từng phần tử vào result mớ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

js
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

js
const arr = [1];
arr.push(2);     // ✅ hợp lệ
arr[0] = 99;     // ✅ hợp lệ
arr = [3];       // ❌ TypeError

Tạ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

js
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"

js
const arr = [1, , 3];
console.log(arr.length); // 3
console.log(arr[1]);     // undefined

Tạ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

js
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.

js
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):

  • myCart vẫn là ["apple", "banana"] sau khi gọi clearCart.
  • 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ì:

  1. cart nhận copy của reference (trỏ đến cùng object).
  2. cart = [] tạo array mới và gán cart trỏ sang đó.
  3. myCart vẫn trỏ đến array cũ.

Verification (Xác minh):

js
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)

js
function clearCart(cart) {
  cart.length = 0;
  return cart;
}

Fix (Sửa) — Option 2: Return new array (không mutate)

js
function clearCart(cart) {
  return [];
}

const myCart = ["apple", "banana"];
const emptyCart = clearCart(myCart);
// myCart vẫn còn, emptyCart là mới

Prevention (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ư createEmptyCart hoặc clearCartCopy, 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.

js
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;
}
js
// 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:

  1. Option A có lợi gì về performance?
  2. Option B có lợi gì về predictability?
  3. 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ĩ:

    1. Option A không tạo array mới (trừ khi cần splice), tiết kiệm bộ nhớ tạm thời.
    2. Option B: recent gốc không bao giờ đổi. Các component khác đang reference recent sẽ không bị ảnh hưởng. Dễ test: cho input, so sánh output.
    3. Option B an toàn hơn. Nếu Component A đang render recent, Component B gọi addToRecent, 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:

js
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:

  1. Tại sao notification "biến mất" khỏi giữa danh sách?
  2. addNotification là pure hay impure? Tại sao?
  3. Viết lại addNotification để không mutate notifications gốc.
[Đáp án tham khảo]
  • Bạn nghĩ:

    1. shift() xóa phần tử đầu danh sách khi length > 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 đổi length.
    2. Impure. push và shift đều mutate list (và notifications gốc vì cùng reference). Có side effect observable bên ngoài.
    3. 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

  1. 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 slice nhưng sau đó mutate phần tử bên trong.
    js
    const items = [{ name: "A" }, { name: "B" }];
    const copy = items.slice();
    copy[0].name = "Z";
    console.log(items[0].name);
  2. Hỏi AI: "Đoạn code này có mutate items gốc không? Tại sao?"
  3. 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?
  4. Verify bằng MDN: tìm Array.prototype.slice trê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ĩ: items bị 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 .name qua copy[0] cũng sửa object gốc. Đây là shallow copy bug.

  • AI trả lời (typical):

    • slice creates 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].name will 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ủa slice — đâ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 items là 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ậy const có ý 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: const khó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 const như 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 + push hợp lệ không?
  • Bạn có tránh được việc nói "array là mutable nên dùng let" không? (const vẫ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/spread tạo mới, hoặc Object.freeze.)
  • Nếu đồng nghiệp hỏi: const copy = arr.slice(); copy[0].name = "Z" có mutate arr khô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 BackTeach Back Section
Debug reassignment vs mutation confusion (Gỡ lỗi nhầm lẫn reassign/mutate)Debug LabDebug Lab Section
Choose mutating vs non-mutating approach (Chọn approach mutate/non-mutate)Design DecisionDesign 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ép arr.push() (const khóa binding, không khóa mutation).
  • [ ] Có thể viết function xử lý array bằng slice và 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ừ forEach có 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 slice và 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.
📴 Offline Mode — Content served from cache