Lesson 0.6.3 — Array Iteration
0. Metadata
| Field | Value |
|---|---|
| Stage | 0 — JavaScript Language Foundation |
| Module | 0.6 — Data Structures |
| Lesson | 0.6.3 |
| Competency | C01.6 — Arrays / Objects |
| Depth Target | L2–L3 |
| Prerequisites | Arrays (0.6.2), Callback (0.5.5), Functions (0.5.1–0.5.3) |
| Estimated Cognitive Load | Medium–High |
1. Why This Exists (Vì sao cần học)
Bạn đã biết tạo array, thêm/xóa phần tử, cắt mảng. Nhưng trong thực tế, 90% thời gian bạn không thao tác từng phần tử bằng for loop — bạn biến đổi toàn bộ collection.
Xét đoạn code:
const users = [
{ name: "Alice", age: 24, active: true },
{ name: "Bob", age: 17, active: false },
{ name: "Carol", age: 30, active: true }
];
// Lấy tên người dùng đang active
const names = [];
for (let i = 0; i < users.length; i++) {
if (users[i].active) {
names.push(users[i].name);
}
}Vấn đề cốt lõi
Vòng lặp for này chạy đúng nhưng nói sai. Nó nói: "Tôi đang đếm index, kiểm tra điều kiện, push vào mảng khác." Nhưng intent thực sự chỉ là: "Lấy tên của user active."
Array iteration methods (map, filter, find, v.v.) cho phép bạn viết code phản ánh intent thay vì cơ chế. Bài này dạy bạn chọn đúng method cho đúng mục đích — và tránh lỗi cực kỳ phổ biến: dùng forEach như map, hoặc quên return trong map.
2. Prerequisites (Yêu cầu đầu vào)
Trước khi học bài này, bạn cần:
- Tạo array, truy cập index, biết
length(0.6.2). - Hiểu array là mutable object (0.6.2).
- Biết callback là function truyền vào function khác (0.5.5).
- Biết arrow function cơ bản (0.5.3).
WARNING
Nếu bạn chưa chắc users.map(user => user.name) trả về gì, hoặc chưa hiểu tại sao forEach khác map, hãy đọc kỹ bài này. Đây là nơi Junior thường "chạy được nhưng không hiểu tại sao".
3. Learning Objectives (Mục tiêu học tập)
Sau bài này, bạn có thể:
- Chọn đúng iteration method cho intent: transform (
map), select (filter), search (find,findIndex), test (some,every,includes). - Phân biệt
forEach(side effect, không return) vàmap(transform, return array mới). - Dự đoán output của
mapkhi callback quênreturn. - Viết callback đúng signature
(item, index, array)cho từng method. - Nhận diện khi nào nên chain
filter→mapthay vì một vòngfor. - Tránh mutate phần tử bên trong
map(shallow copy trap).
4. Mental Model (Mô hình tư duy)
Mental Model
Array Iteration = Đưa từng phần tử qua một "máy kiểm tra"
Input Array
↓
┌─────────────┐
│ map() │ → Transform từng phần tử → Array mới, cùng length
└─────────────┘
↓
┌─────────────┐
│ filter() │ → Giữ lại phần tử pass test → Array mới, length ≤ input
└─────────────┘
↓
┌─────────────┐
│ find() │ → Tìm phần tử đầu tiên pass test → Phần tử hoặc undefined
└─────────────┘
↓
┌─────────────┐
│ some/every │ → Kiểm tra điều kiện trên collection → Boolean
└─────────────┘
↓
┌─────────────┐
│ forEach() │ → Làm việc gì đó với từng phần tử → undefined
└─────────────┘
↓
Không tạo array mới
(Dùng cho side effect: log, DOM, mutate external)Quy tắc chọn method:
| Intent | Method |
|---|---|
| "Tôi muốn biến đổi mỗi phần tử thành cái khác" | map |
| "Tôi muốn giữ lại một số phần tử" | filter |
| "Tôi muốn tìm một phần tử" | find |
| "Tôi muốn biết có ít nhất một phần tử thỏa mãn" | some |
| "Tôi muốn biết tất cả phần tử đều thỏa mãn" | every |
| "Tôi muốn làm gì đó, không cần array mới" | forEach |
Quy tắc vàng: Nếu bạn dùng forEach rồi push vào array bên ngoài, bạn có thể đang cần map hoặc filter.
5. Core Concepts (Các khái niệm cốt lõi)
Essential (Bắt buộc)
| Concept | Ý nghĩa |
|---|---|
forEach(callback) | Gọi callback cho từng phần tử. Không return. Không tạo array mới. |
map(callback) | Tạo array mới từ kết quả callback. Length = length input. Quên return → undefined. |
filter(callback) | Tạo array mới chỉ chứa phần tử mà callback trả về truthy. Có thể rỗng. |
find(callback) | Trả về phần tử đầu tiên pass test. Không có → undefined. |
findIndex(callback) | Trả về index đầu tiên pass test. Không có → -1. Dùng callback giống find. Khác indexOf: findIndex test bằng callback, indexOf so sánh strict equality với một giá trị cụ thể. |
some(callback) | true nếu ít nhất một phần tử pass test. |
every(callback) | true nếu tất cả phần tử pass test. Mảng rỗng → true. |
includes(value) | true nếu array chứa value (strict equality ===). |
Supporting (Hỗ trợ)
| Concept | Ý nghĩa |
|---|---|
| Callback signature | (item, index, array) => .... Thường chỉ cần item. |
| Method chaining | arr.filter(...).map(...) — kết hợp nhiều bước. |
| Truthy return | filter giữ phần tử nếu callback trả về truthy (không nhất thiết true). |
Awareness (Biết tồn tại)
| Concept | Lý do chưa đào sâu |
|---|---|
flatMap | Kết hợp map + flat. Học ở Stage 2. |
Async callback trong forEach | forEach không await được đúng cách. Dùng for...of (Khi cần chạy tuần tự từng cái). Dùng Promise.all và map (Khi cần chạy song song cùng lúc). Thuộc Stage 3 (Async). |
Out of Scope (Không thuộc bài này)
reduce/reduceRight— 0.6.4.sort,reverse(mutate) — đã đề cập ở 0.6.2, không lặp lại.findLast,findLastIndex(ES2023) — awareness, không cần master.- Custom iteration protocol (
Symbol.iterator) — Stage 2.
6. Worked Example (Ví dụ phân tích từng bước)
Bài toán: Từ danh sách đơn hàng, tạo danh sách email xác nhận cho các đơn hàng đã hoàn thành.
const orders = [
{ id: 1, customer: "Alice", total: 150, status: "completed" },
{ id: 2, customer: "Bob", total: 80, status: "pending" },
{ id: 3, customer: "Carol", total: 200, status: "completed" }
];Yêu cầu:
- Chỉ lấy đơn
completed. - Tạo object
{ email, amount }cho mỗi đơn. - Nếu không có đơn completed, kết quả là mảng rỗng.
const notifications = [];
for (let i = 0; i < orders.length; i++) {
if (orders[i].status === "completed") {
notifications.push({
email: orders[i].customer.toLowerCase() + "@shop.com",
amount: orders[i].total
});
}
}const notifications = orders
.filter(order => order.status === "completed")
.map(order => ({
email: `${order.customer.toLowerCase()}@shop.com`,
amount: order.total
}));Walkthrough
Step 1 — Chọn filter Intent: "Giữ lại đơn completed." filter nhận callback trả về boolean/truthy. Kết quả: [order 1, order 3].
Step 2 — Chọn map Intent: "Biến mỗi đơn thành object email." map tạo array mới cùng length. Kết quả: [{email: "alice@shop.com", amount: 150}, {email: "carol@shop.com", amount: 200}].
Step 3 — Chainfilter trả về array mới → array đó có .map(). Đọc từ trái sang phải: "Lọc completed, rồi map sang email."
Step 4 — So sánh với forfor loop nói về cơ chế (index, push). filter + map nói về intent (chọn, biến đổi). Khi debug, bạn chỉ cần kiểm tra từng bước chain thay vì trace index.
Key Insight: Mỗi method là một "verb" mô tả hành động trên collection. Chọn đúng verb = code tự giải thích.
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 nums = [1, 2, 3];
const doubled = nums.map(n => { n * 2; });
console.log(doubled);Câu 2
const users = [{ name: "A" }, { name: "B" }];
const names = users.map(u => u.name);
users[0].name = "Z";
console.log(names);Câu 3
const scores = [50, 80, 65, 90];
const passed = scores.filter(s => s >= 70);
console.log(passed.length);
console.log(scores.length);Câu 4
const items = ["a", "b", "c"];
const found = items.find(i => i === "d");
const index = items.findIndex(i => i === "d");
console.log(found, index);Câu 5
const arr = [0, "", false, null, undefined];
const result = arr.filter(Boolean);
console.log(result);[Đáp án & Giải thích]
Câu 1: [undefined, undefined, undefined] — Quên return trong arrow block
- Giải thích: Arrow function có block
{}cầnreturnexplicit.n * 2là expression nhưng không được return.mapcallback trả vềundefinedcho mỗi phần tử. Nếu muốn implicit return, bỏ block:n => n * 2.
Câu 2: ["A", "B"] — map tạo array mới nhưng string là primitive
- Giải thích:
nameslà["A", "B"]vìu.namelúc đó là"A"và"B". Sau đóusers[0].name = "Z"mutate object trongusers, nhưngnamesđã chứa string primitive"A"(được copy by value).nameskhông đổi. Nếunameschứa object references thì sẽ khác.
Câu 3: 2 và 4 — filter không mutate
- Giải thích:
filtertạo mảng mớipassed = [80, 90].scoresgốc không đổi, length vẫn 4.
Câu 4: undefined và -1 — Không tìm thấy
- Giải thích:
findkhông có match →undefined.findIndexkhông có match →-1. Khác vớiindexOftrả về-1khi không tìm thấy primitive.
Câu 5: [] — Truthy filter
- Giải thích:
Booleanlà constructor/function.filter(Boolean)giữ phần tử nếuBoolean(item)làtrue. Tất cả các phần tử0,"",false,null,undefinedđều falsy. Kết quả rỗng.
8. Implementation Lab (Bài lab thực hành)
Level 1 — Guided (Có hướng dẫn)
Viết function getActiveUserNames bằng filter → map chain.
const users = [
{ name: "Alice", active: true },
{ name: "Bob", active: false },
{ name: "Carol", active: true }
];
function getActiveUserNames(users) {
// 1. filter giữ user có active === true
// 2. map lấy user.name
// 3. return kết quả
}
console.log(getActiveUserNames(users));
// ["Alice", "Carol"]Gợi ý
users.filter(u => u.active).map(u => u.name) — đọc từ trái sang phải: "lọc active, rồi lấy tên."
[Đáp án tham khảo]
function getActiveUserNames(users) {
return users
.filter(user => user.active)
.map(user => user.name);
}
const users = [
{ name: "Alice", active: true },
{ name: "Bob", active: false },
{ name: "Carol", active: true }
];
console.log(getActiveUserNames(users));
// ["Alice", "Carol"]Giải thích:
filter(user => user.active)giữ lại object cóactivetruthy..map(user => user.name)trên kết quả filter, tạo mảng string.usersgốc không đổi. Cả hai method đều non-mutating.
Level 2 — Partial Scaffold (Khung sẵn)
Hoàn thành function summarizeOrders để trả về object thống kê.
function summarizeOrders(orders) {
// orders: [{ id, amount, status: "completed" | "pending" }]
const completed = orders._______(o => o.status === "completed");
let total = 0;
completed._______(o => {
total += o.amount;
});
// Sẽ dùng .reduce() để tính total
// sau khi học tới Lesson 0.6.4 — Reduce
const hasLargeOrder = orders._______(o => o.amount > 1000);
return {
count: completed.length,
total,
hasLargeOrder
};
}
// Sử dụng:
const orders = [
{ id: 1, amount: 500, status: "completed" },
{ id: 2, amount: 1200, status: "pending" },
{ id: 3, amount: 800, status: "completed" }
];
console.log(summarizeOrders(orders));
// { count: 2, total: 1300, hasLargeOrder: true }[Đáp án tham khảo]
function summarizeOrders(orders) {
const completed = orders.filter(o => o.status === "completed");
let total = 0;
completed.forEach(o => {
total += o.amount;
});
const hasLargeOrder = orders.some(o => o.amount > 1000);
return {
count: completed.length,
total,
hasLargeOrder
};
}
const orders = [
{ id: 1, amount: 500, status: "completed" },
{ id: 2, amount: 1200, status: "pending" },
{ id: 3, amount: 800, status: "completed" }
];
console.log(summarizeOrders(orders));
// { count: 2, total: 1300, hasLargeOrder: true }Giải thích:
filterlấy đơn completed.completed.lengthcho biết số lượng.forEachdùng để tính tổng vì ta cần side effect (cộng dồn vàototal). Đây là trường hợp hợp lý để dùngforEach.somekiểm tra có ít nhất một đơn > 1000. Trả về boolean.- Không dùng
mapđể tính tổng vìmapbắt buộc trả về giá trị cho mỗi phần tử — không phải intent.
Level 3 — Independent (Tự viết)
Viết các function sau (không dùng for loop, chỉ dùng iteration methods):
// 1. pluck(array, key) → Trả về mảng các giá trị tại property `key`
// Ví dụ:
// pluck([{a: 1}, {a: 2}], "a")
// → [1, 2]
// 2. partition(array, predicate) → Trả về { pass: [], fail: [] }
// Ví dụ:
// partition([1, 2, 3, 4], n => n > 2)
// → { pass: [3, 4], fail: [1, 2] }
// 3. uniqueBy(array, key)
// → Trả về mảng không trùng lặp theo property `key`,
// giữ phần tử đầu tiên
// Ví dụ:
// uniqueBy([{id: 1}, {id: 2}, {id: 1}], "id")
// → [{id: 1}, {id: 2}]
// Sử dụng:
const products = [
{ id: 1, name: "A", price: 100 },
{ id: 2, name: "B", price: 200 },
{ id: 1, name: "C", price: 150 }
];
console.log(pluck(products, "name")); // ["A", "B", "C"]
console.log(partition(products, p => p.price > 120));
// {
// pass: [
// {id: 2, name: "B", price: 200},
// {id: 1, name: "C", price: 150}
// ],
// fail: [{id: 1, name: "A", price: 100}]
// }
console.log(uniqueBy(products, "id"));
// [
// { id: 1, name: "A", price: 100 },
// { id: 2, name: "B", price: 200 }
// ]Gợi ý
pluck: dùngmap.partition: dùngforEachhoặcreduce(nếu đã biết) để phân loại vào hai mảng (hoặcfilterhai lần).uniqueBy: dùngforEach. Với mỗi item, nếu giá trị tạikeychưa có trongseenthì push item vàoresultvà push giá trị vàoseen.
[Đáp án tham khảo]
function pluck(array, key) {
return array.map(item => item[key]);
}
function partition(array, predicate) {
const pass = [];
const fail = [];
array.forEach(item => {
if (predicate(item)) {
pass.push(item);
} else {
fail.push(item);
}
});
return { pass, fail };
}
function uniqueBy(array, key) {
const result = [];
const seen = [];
array.forEach(item => {
const value = item[key];
if (!seen.includes(value)) {
seen.push(value);
result.push(item);
}
});
return result;
}
const products = [
{ id: 1, name: "A", price: 100 },
{ id: 2, name: "B", price: 200 },
{ id: 1, name: "C", price: 150 }
];
console.log(pluck(products, "name"));
// ["A", "B", "C"]
console.log(partition(products, p => p.price > 120));
// {
// pass: [
// { id: 2, name: "B", price: 200 },
// { id: 1, name: "C", price: 150 }
// ],
// fail: [
// { id: 1, name: "A", price: 100 }
// ]
// }
console.log(uniqueBy(products, "id"));
// [
// { id: 1, name: "A", price: 100 },
// { id: 2, name: "B", price: 200 }
// ]Giải thích:
pluck:mapbiến mỗi object thànhitem[key].partition:forEachphân loại trực tiếp vào hai mảngpass/fail. DùngforEachhợp lý vì ta cần side effect (push vào mảng ngoài) thay vì tạo giá trị return mới.uniqueBy:forEachduyệt từng item. Nếu giá trị tạikeychưa có trongseen(includestrả vềfalse), push item vàoresultvà push giá trị key vàoseen.💡
Setsẽ được học ở Module 2.7 (Built-in Object Structures) với hiệu suất tốt hơn cho trường hợp này.jsfunction uniqueBy(array, key) { const seen = new Set(); return array.filter(item => { const value = item[key]; if (seen.has(value)) { return false; } seen.add(value); return true; }); }Điểm tốt hơn (nếu dùng
Set):Array + includesSet+hasseen.includes(value)duyệt tuyến tính toàn bộ mảngseenseen.has(value)tra cứu trực tiếp (hash-based)O(n²) tổng thể — với mỗi phần tử, có thể duyệt lại cả mảng seenO(n) tổng thể — mỗi lần kiểm tra là O(1) Với 10.000 phần tử: ~50 triệu phép so sánh Với 10.000 phần tử: ~10.000 phép so sánh Phù hợp Stage 0 (đã học array) Thuộc Module 2.7 ( Set)
9. Edge Cases (Các trường hợp ngoại lệ)
map quên return trong block body
const nums = [1, 2, 3];
const bad = nums.map(n => { n * 2; }); // [undefined, undefined, undefined]
const good = nums.map(n => n * 2); // [2, 4, 6]
const alsoGood = nums.map(n => { return n * 2; });Tại sao: Arrow function có block {} cần return explicit. Không có return → callback trả về undefined.
Cách nhận biết: Array kết quả toàn undefined dù logic có vẻ đúng.
Cách xử lý: Bỏ block nếu chỉ có một expression, hoặc thêm return.
find với object reference
const users = [{ id: 1 }, { id: 2 }];
const target = { id: 1 };
const found = users.find(u => u === target);
console.log(found); // undefinedTại sao: find so sánh bằng strict equality (===). target là object mới, khác reference với { id: 1 } trong array. So sánh object phải so sánh property.
Cách nhận biết: find luôn trả về undefined dù object "trông giống nhau".
Cách xử lý: So sánh property: users.find(u => u.id === target.id).
Mutate phần tử trong map
const users = [{ name: "A" }, { name: "B" }];
users.map(u => {
u.name = u.name.toLowerCase();
return u;
});Tại sao: map trả về array mới nhưng phần tử bên trong vẫn là cùng object reference. Mutate u.name sửa object gốc trong users. Đây là shallow copy trap.
Cách nhận biết: users gốc bị thay đổi sau khi map.
Cách xử lý: Trả về object mới: users.map(u => ({ ...u, name: u.name.toLowerCase() })).
forEach không thể break
const nums = [1, 2, 3, 4];
let found;
nums.forEach(n => {
if (n > 2) {
found = n;
break; // ❌ SyntaxError
}
});Tại sao: break chỉ dùng trong vòng lặp for/while/switch. forEach là function call, không phải loop statement.
Cách nhận biết: SyntaxError hoặc logic không dừng khi cần.
Cách xử lý: Dùng find nếu cần tìm phần tử đầu tiên thỏa mãn. Dùng some nếu chỉ cần kiểm tra tồn tại.
every với mảng rỗng
const empty = [];
console.log(empty.every(x => x > 0)); // trueTại sao: every trả về true cho mảng rỗng (vacuous truth — không có phần tử nào vi phạm điều kiện).
Cách nhận biết: Logic "tất cả đều pass" trả về true dù không có phần tử nào.
Cách xử lý: Nếu cần đảm bảo mảng không rỗng, kiểm tra length trước: arr.length > 0 && arr.every(...).
10. Debug Lab (Bài lab gỡ lỗi)
Symptom (Triệu chứng): Danh sách sản phẩm hiển thị sai sau khi format giá.
function formatProducts(products) {
return products.map(product => {
product.price = `$${product.price}`;
return product;
});
}
const catalog = [
{ name: "Book", price: 10 },
{ name: "Pen", price: 2 }
];
const display = formatProducts(catalog);
console.log(display);
// [{ name: "Book", price: "$10" }, { name: "Pen", price: "$2" }]
console.log(catalog[0].price);
// "$10" ❌ — catalog gốc bị sửa!Reproduction (Tái hiện lỗi): Chạy code trên.
Evidence (Bằng chứng):
catalog[0].pricethành"$10"dù chỉ gọiformatProducts.displayvàcatalogchứa cùng object references.
Hypothesis (Giả thuyết): map tạo array mới nhưng callback mutate product object gốc. product.price = ... sửa property trên object mà catalog cũng đang trỏ đến.
Verification (Xác minh):
console.log(display[0] === catalog[0]); // true — cùng referenceRoot Cause (Nguyên nhân gốc rễ): Nhầm lẫn "array mới" với "deep copy". map tạo array mới nhưng phần tử vẫn là reference cũ. Mutate trong callback = mutate gốc. Đây là lỗi production phổ biến khi render list trong React.
Fix (Sửa):
function formatProducts(products) {
return products.map(product => ({
...product,
price: `$${product.price}`
}));
}Prevention (Phòng ngừa):
- Trong
map, luôn hỏi: "Callback có sửaitemkhông?" - Nếu cần thay đổi object, trả về object mới bằng spread.
- Trong code review, nghi ngờ mọi assignment trên
itembên trongmap/filter.
11. Design Exercise (Bài tập thiết kế giải pháp)
Bạn có danh sách user và cần tìm user đầu tiên có email hợp lệ (chứa @).
const validUser = users.filter(u => u.email.includes("@"))[0];const validUser = users.find(u => u.email.includes("@"));Câu hỏi:
- Option A có vấn đề gì về performance?
- Option B có lợi gì về semantics?
- Nếu
userscó 10,000 phần tử và user đầu tiên đã hợp lệ, Option A làm bao nhiêu việc thừa?
[Đáp án tham khảo]
Bạn nghĩ:
- Option A chạy
filterqua toàn bộ array (10,000 lần check), tạo mảng mới chứa tất cả user hợp lệ, rồi lấy[0]. Nếu chỉ cần một user, đây là lãng phí. - Option B dừng ngay khi tìm thấy phần tử đầu tiên. Semantics rõ ràng: "tìm một user" thay vì "lọc tất cả rồi lấy đầu tiên".
- Option A thực hiện ~10,000 checks + cấp phát mảng mới. Option B thực hiện 1 check và dừng. Chênh lệch rất lớn.
- Option A chạy
Kết luận: Chọn method phù hợp intent không chỉ là readability — nó ảnh hưởng performance.
findcho "tìm một",filtercho "lấy tất cả match".
12. Production Scenario (Tình huống thực tế)
Context: Dashboard hiển thị bảng đơn hàng. Developer viết:
const columns = orders.map(order => {
order.displayTotal = formatCurrency(order.total);
order.statusLabel = getStatusLabel(order.status);
return order;
});Symptom: Sau khi vào trang dashboard, quay lại trang chi tiết đơn hàng — giá hiển thị dạng "$1,500.00" thay vì 1500 (số). Hoặc API gửi lên server bị lỗi validation vì statusLabel không phải enum hợp lệ.
Constraint: orders được fetch từ API và cache để dùng ở nhiều component.
Câu hỏi:
- Tại sao dữ liệu gốc bị "nhiễm" display properties?
mapở đây có phải anti-pattern không? Nếu có, tại sao?- Viết lại đúng.
[Đáp án tham khảo]
Bạn nghĩ:
mapcallback mutateorderobject gốc.orderslà array reference từ cache. Các component khác đọc cùng object nên thấy property mới.- Đúng, đây là anti-pattern: dùng
mapđể mutate side effect thay vì transform.mapnên trả về object mới. Nếu chỉ cần side effect (thêm property để render),forEachcũng không tốt hơn vì vẫn mutate cache. - jsHoặc tách ra:
const columns = orders.map(order => ({ ...order, displayTotal: formatCurrency(order.total), statusLabel: getStatusLabel(order.status) }));forEachkhông phải lựa chọn ở đây vì vẫn mutate. Cần tạo view model mới.
Bài học:
maplà transform, không phải "loop để sửa". Nếu data được share, mutate trong iteration = corruption cross-component.
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
mapnhưng callback quênreturn. Dự đoán output.jsconst nums = [1, 2, 3]; const result = nums.map(n => { n * 2; }); console.log(result); - Hỏi AI: "Tại sao kết quả là [undefined, undefined, undefined]?"
- So sánh câu trả lời AI với nhận định của bạn. AI có nhấn mạnh sự khác biệt giữa
n => n * 2vàn => { n * 2; }không? - Verify bằng MDN: tìm "Arrow functions" trên MDN, đọc phần "Function body".
Gợi ý
AI thường biết câu trả lời đúng nhưng đôi khi giải thích mơ hồ bằng cách nói "bạn cần return". Nếu AI không chỉ ra rằng đây là sự khác biệt giữa expression body và block body của arrow function — một quy tắc syntax cốt lõi — bạn đã tìm ra điểm mù. AI cũng có thể nhầm lẫn với forEach behavior.
Đáp án tham khảo
Bạn nghĩ:
n => { n * 2; }là block body. Block body không có implicit return.n * 2;là expression statement, không return giá trị. Kết quả callback trả vềundefinedcho mỗi phần tử.AI trả lời (typical):
- You forgot to return the value.
- With curly braces, you need an explicit
return. - Without curly braces, the expression is implicitly returned.
- The result is
[undefined, undefined, undefined].
So sánh: AI thường đưa ra rule đúng nhưng đôi khi không giải thích tại sao JavaScript thiết kế như vậy: block body
{}cho phép nhiều statement nên không thể đoán expression cuối là return value. Expression body (no block) chỉ có một expression nên implicit return là an toàn.Điểm AI nói sai hoặc quá mơ hồ: AI hiếm khi nói rõ: "Đây không phải 'quên' return — đây là hai dạng arrow function hoàn toàn khác nhau về semantics. Dùng block body khi cần side effect hoặc nhiều bước; dùng expression body khi chỉ cần transform và return."
Kết luận: Nếu bạn chỉ ra được rằng
map+ block body là intentional syntax choice, không phải "lỗi nhỏ", bạn đã hiểu sâu hơn AI. Concept này sẽ quay lại ở Stage 8 (React —setStatecallback, event handlers) và Stage 6 (TypeScript — return type inference khác nhau giữa hai dạng).
14. Teach Back (Dạy lại)
Giả sử một junior developer hỏi bạn:
"Em thấy
forEachvàmapđều chạy qua từng phần tử. Vậy khi nào em dùng cái nào? Và tại sao người ta bảo dùngmap/filtertốt hơnfor?"
Hãy giải thích trong 2 phút, dùng đúng terminology: intent, transform, side effect, return value.
Mô phỏng
- Bạn nói:
forEachlà "làm gì đó" — log ra console, gửi request, cộng dồn vào biến ngoài. Nó không trả về gì cả, chỉ chạy vì tác dụng phụ.maplà "biến đổi mỗi phần tử thành cái khác" — nó luôn trả về array mới, cùng độ dài.
Quy tắc: Nếu bạn thấy mình dùng forEach rồi push vào mảng bên ngoài, bạn đang cần map hoặc filter. Nếu bạn dùng map nhưng không dùng kết quả trả về, bạn đang cần forEach.
Về for loop: nó nói về cơ chế — đếm index, tăng biến. map/filter nói về mục đích — "tôi muốn biến đổi" hay "tôi muốn lọc". Code nói về intent thì dễ đọc hơn, dễ debug hơn, và ít bug off-by-one.
💡 Tưởng tượng
forEachnhư một phát thanh viên đọc tên từng người trong danh sách — anh ta không tạo ra danh sách mới.mapnhư một dịch giả — nhận danh sách tiếng Anh, trả về danh sách tiếng Việt, danh sách gốc vẫn còn.
Gợi ý đánh giá bản thân
- Đồng nghiệp có hiểu tại sao
forEachkhông return không? - Bạn có tránh được việc nói "
mapnhanh hơnfor" không? (Không nhất thiết — lý do chính là semantics, không phải performance.) - Nếu đồng nghiệp hỏi: "Vậy
forEachđể làm gì?" — bạn trả lời được không? (Gợi ý: side effect — log, DOM, gọi API, cộng dồn vào accumulator khi chưa họcreduce.) - Nếu đồng nghiệp hỏi: "Tại sao
users.filter(...)[0]không tốt bằngusers.find(...)?" — bạn giải thích được performance và semantics trong 30 giây không?
15. Assessment (Đánh giá)
| Objective (Mục tiêu) | Hình thức đánh giá | Task |
|---|---|---|
| Choose correct iteration method (Chọn method đúng intent) | Prediction (Dự đoán) | Prediction Exercise + Design Exercise |
Distinguish forEach vs map (Phân biệt forEach/map) | Prediction (Dự đoán) | Prediction Câu 1 |
Predict map with missing return (Dự đoán map thiếu return) | Prediction (Dự đoán) | Prediction Câu 1 |
Chain filter → map | Implementation (Thực hành) | Implementation Lab Level 1 |
Use find/some correctly (Dùng find/some đúng) | Implementation (Thực hành) | Implementation Lab Level 2 |
| Avoid mutating in iteration (Tránh mutate trong iteration) | Debug Lab | Debug Lab Section |
Write pluck/partition/uniqueBy | Implementation (Thực hành) | Implementation Lab Level 3 |
| Explain iteration semantics | Teach Back | Teach Back Section |
16. Exit Criteria (Tiêu chí qua bài)
- [ ] Có thể chọn đúng method cho intent:
map(transform),filter(select),find(search one),some/every(test),forEach(side effect). - [ ] Có thể dự đoán output của
mapkhi callback quênreturntrong block body. - [ ] Có thể viết chain
filter→mapđể xử lý collection theo pipeline. - [ ] Có thể giải thích tại sao
forEachkhông thể thay thếmapkhi cần array mới. - [ ] Có thể tránh mutate object bên trong
mapcallback (shallow copy trap). - [ ] Có thể debug lỗi "array mới nhưng object bên trong vẫn bị sửa".
- [ ] Có thể chọn giữa
findvàfilter+[0]dựa trên performance và semantics. - [ ] Có thể viết callback đúng signature
(item, index, array)khi cần.
17. Spiral Connection (Liên kết xoắn ốc)
Previous (Trước): Arrays (0.6.2) — bạn đã biết tạo array, thêm/xóa,
slice, spread. Giờ bạn học cách xử lý toàn bộ array bằng functional iteration thay vì vòng lặp thủ công.
Current (Hiện tại): Array Iteration —
forEach,map,filter,find,findIndex,some,every,includes. Chọn method đúng intent, tránh mutate trong callback, hiểu return value.
Next (Tiếp theo):
- 0.6.4 (Reduce) — Tổng hợp array thành một giá trị duy nhất: sum, group, index.
reducelà "swiss army knife" nhưng cũng dễ abuse.- 0.6.5 (Objects) — Object iteration khác array:
Object.keys,values,entries. Sẽ dùng lạimap/filtertrên array từObject.entries.- Stage 1 (Execution Model) — Callback trong iteration method tạo scope mới mỗi lần gọi. Hiểu execution context giúp debug closure trong loop.
- Stage 8 (React) —
mapđể render list là pattern bắt buộc. Mutate trongmap= violate React immutability.keyprop liên quan đến identity tracking trong reconciliation.