Lesson 0.6.4 — Reduce
0. Metadata
| Field | Value |
|---|---|
| Stage | 0 — JavaScript Language Foundation |
| Module | 0.6 — Data Structures |
| Lesson | 0.6.4 |
| Competency | C01.6 — Arrays / Objects |
| Depth Target | L2–L3 |
| Prerequisites | Array Iteration (0.6.3), Arrays (0.6.2), Callback (0.5.5) |
| Estimated Cognitive Load | Medium–High |
1. Why This Exists (Vì sao cần học)
Bạn đã biết map biến đổi từng phần tử thành array mới, filter chọn lọc phần tử. Nhưng trong thực tế, bạn thường cần tổng hợp toàn bộ array thành một giá trị duy nhất: tính tổng tiền, đếm số lượng, nhóm theo danh mục, hoặc biến array thành object lookup.
Xét đoạn code:
const orders = [
{ id: 1, amount: 100, category: "food" },
{ id: 2, amount: 200, category: "electronics" },
{ id: 3, amount: 50, category: "food" }
];
let total = 0;
for (const order of orders) {
total += order.amount;
}Vấn đề cốt lõi
Vòng lặp này mang một biến total "bên ngoài" và cộng dồn vào. Nó chạy đúng nhưng biến total là state ngoài luồng — không được trả về từ một expression, khó chain, và dễ bị sửa nhầm ở chỗ khác.
reduce giải quyết vấn đề này bằng cách đưa toàn bộ accumulation vào một function: mỗi bước nhận "kết quả tạm" và "phần tử hiện tại", trả về "kết quả tạm tiếp theo". Không còn biến ngoài luồng.
Nhưng reduce cũng là method bị abuse nhiều nhất: dùng để làm việc của map, filter, hoặc viết thành một dòng không thể đọc. Bài này dạy bạn dùng reduce đúng mục đích: aggregation — gom collection thành một giá trị**.
2. Prerequisites (Yêu cầu đầu vào)
Trước khi học bài này, bạn cần:
- Hiểu
map,filter,forEachvà callback signature(item, index, array)(0.6.3). - Biết array là mutable object, spread tạo shallow copy (0.6.2).
- Biết arrow function implicit vs explicit return (0.5.3).
- Hiểu
constvà binding (0.2.2).
WARNING
Nếu bạn chưa chắc tại sao map cần return trong block body, quay lại 0.6.3. reduce callback cũng bắt buộc return giá trị mới cho accumulator — thiếu return là lỗi phổ biến nhất.
3. Learning Objectives (Mục tiêu học tập)
Sau bài này, bạn có thể:
- Giải thích mental model của
reduce: accumulator, current value, next accumulator, final result. - Viết
reduceđể tính tổng, đếm, nhóm, và index-by-ID. - Phân biệt khi nào dùng
reduce(aggregation) và khi nào dùngmap/filter(transform/select). - Dự đoán kết quả khi thiếu
initialValuehoặc quênreturntrong callback. - Tránh dùng shared object/array từ bên ngoài làm
initialValue, và phân biệt mutate accumulator tạm (literal trong reduce) với mutate shared reference. - Nhận diện khi
reducebị overkill (ví dụ: dùngreducethay chomap).
4. Mental Model (Mô hình tư duy)
Mental Model
reduce = Máy gói quà đi qua băng chuyền
Băng chuyền: [A] → [B] → [C] → [D]
↓
Túi tích lũy (Accumulator) bắt đầu với giá trị khởi tạo
↓
Step 1: Túi (init) + A → Túi mới (acc₁)
Step 2: Túi (acc₁) + B → Túi mới (acc₂)
Step 3: Túi (acc₂) + C → Túi mới (acc₃)
Step 4: Túi (acc₃) + D → Túi cuối (result)
Kết quả cuối cùng = Nội dung túi sau khi qua hết băng chuyềnCấu trúc callback:
array.reduce((accumulator, currentValue, index, array) => {
return nextAccumulator;
}, initialValue);Quy tắc vàng:
reducekhông dùng để "loop và làm gì đó" — đó làforEach.reducekhông dùng để "biến đổi từng phần tử thành array" — đó làmap.reducedùng để "gom tất cả phần tử thành một thứ duy nhất": số, object, map, string, array khác.
Accumulator có thể là bất kỳ type nào:
0(number) → sum""(string) → concat[](array) → collect/filter (nhưng cân nhắcfiltertrước){}(object) → group, index
5. Core Concepts (Các khái niệm cốt lõi)
Essential (Bắt buộc)
| Concept | Ý nghĩa |
|---|---|
reduce(callback, initialValue) | Gọi callback cho từng phần tử, truyền accumulator từ bước trước. Trả về accumulator cuối cùng. |
| Accumulator | Giá trị "tích lũy" được truyền từ callback này sang callback tiếp theo. |
| Current Value | Phần tử array đang xử lý. |
| Initial Value | Giá trị khởi tạo cho accumulator. Nếu thiếu, phần tử đầu tiên làm initial và callback bắt đầu từ index 1 (hoặc không chạy nếu array chỉ có 1 phần tử). Luôn cung cấp initial value để tránh silent skip. |
| Return trong callback | Bắt buộc. Callback phải trả về next accumulator. Quên return → undefined lan truyền. |
Supporting (Hỗ trợ)
| Concept | Ý nghĩa |
|---|---|
reduceRight | Giống reduce nhưng duyệt từ phải sang trái. Dùng cho string operations hoặc right-associative. |
| Chaining | filter → map thường đọc rõ hơn reduce làm cả hai. |
Awareness (Biết tồn tại)
| Concept | Lý do chưa đào sâu |
|---|---|
Functional fold/foldl | reduce là implementation của fold trong FP. Theory sâu hơn là Elective. |
| Async reduce | Promise.all + reduce pattern. Thuộc Stage 3 (Async). |
Out of Scope (Không thuộc bài này)
- Dùng
reducethay chomap/filter(anti-pattern, sẽ đề cập trong Design Exercise). reducevới async/await.Array.prototype.flat(đã có native method, không cần reduce).
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ính tổng doanh thu và nhóm theo danh mục.
const orders = [
{ id: 1, amount: 100, category: "food" },
{ id: 2, amount: 200, category: "electronics" },
{ id: 3, amount: 50, category: "food" },
{ id: 4, amount: 300, category: "electronics" }
];Yêu cầu:
- Tổng doanh thu.
- Số lượng đơn hàng theo danh mục:
{ food: 2, electronics: 2 }.
let total = 0;
const countByCategory = {};
for (const order of orders) {
total += order.amount;
if (!countByCategory[order.category]) {
countByCategory[order.category] = 0;
}
countByCategory[order.category]++;
}
console.log(total); // 650
console.log(countByCategory); // { food: 2, electronics: 2 }const total = orders.reduce((sum, order) => sum + order.amount, 0);
const countByCategory = orders.reduce((counts, order) => {
const key = order.category;
counts[key] = (counts[key] || 0) + 1;
return counts;
}, {});
console.log(total); // 650
console.log(countByCategory); // { food: 2, electronics: 2 }Walkthrough
Step 1 — Sum với reduceorders.reduce((sum, order) => sum + order.amount, 0)
- Initial value:
0(number). - Step 1:
sum = 0,order = {amount: 100}→ return100. - Step 2:
sum = 100,order = {amount: 200}→ return300. - Step 3:
sum = 300,order = {amount: 50}→ return350. - Step 4:
sum = 350,order = {amount: 300}→ return650. - Final result:
650.
Step 2 — Count by category với reduceorders.reduce((counts, order) => { ... }, {})
- Initial value:
{}(empty object). - Step 1:
counts = {},order.category = "food"→counts["food"] = (undefined || 0) + 1 = 1, returncounts. - Step 2:
counts = {food: 1},order.category = "electronics"→counts["electronics"] = 1, return. - Step 3:
counts = {food: 1, electronics: 1},order.category = "food"→counts["food"] = 2, return. - Final:
{food: 2, electronics: 2}.
Key Insight: reduce đưa "biến ngoài luồng" (total, countByCategory) thành "biến trong luồng" (accumulator). Kết quả là return value duy nhất, không còn state rời rạc.
Cảnh báo — Mutate accumulator tạm vs shared object: Ở đây ta gán property trên counts ({} được tạo mới trong reduce). Điều này khác với Debug Lab: gán property trên object literal tạm trong reduce là an toàn, nhưng dùng object từ bên ngoài (const initial = {} rồi truyền vào) sẽ gây bug shared state. Xem Edge Cases và Debug Lab.
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 sum = nums.reduce((acc, n) => acc + n);
console.log(sum);Câu 2
const nums = [];
const sum = nums.reduce((acc, n) => acc + n);
console.log(sum);Câu 3
const nums = [1, 2, 3];
const result = nums.reduce((acc, n) => {
acc.push(n * 2);
}, []);
console.log(result);Câu 4
const items = ["a", "b", "c"];
const indexed = items.reduce((acc, item, index) => {
acc[item] = index;
return acc;
}, {});
console.log(indexed);Câu 5
const nums = [1, 2, 3, 4];
const max = nums.reduce((acc, n) => n > acc ? n : acc);
console.log(max);[Đáp án & Giải thích]
Câu 1: 6 — Thiếu initialValue nhưng array không rỗng
- Giải thích: Không có initial value,
acc= phần tử đầu (1),n= phần tử thứ hai (2). Bắt đầu từ index 1. Kết quả1 + 2 + 3 = 6. Đúng nhưng risky — nên luôn cung cấp initial value.
Câu 2: TypeError: Reduce of empty array with no initial value — Empty array + no initialValue
- Giải thích: Array rỗng, không có initial value, không có phần tử đầu để làm
acc. Throw TypeError. Luôn cung cấp initial value cho array có thể rỗng.
Câu 3: TypeError: Cannot read properties of undefined (hoặc tương tự) — Quên return
- Giải thích: Callback không có
return. Arrow function block body mà thiếureturn→ trả vềundefined. Lần thứ haiacclàundefined,acc.pushlỗi. Đây là lỗi #1 củareduce.
Câu 4: { a: 0, b: 1, c: 2 } — Index by value
- Giải thích: Accumulator bắt đầu là
{}. Mỗi bước thêm propertyitem: index. Returnacc. Kết quả object map từ value sang index.
Câu 5: 4 — Thiếu initialValue, dùng phần tử đầu
- Giải thích: Không có initial value,
acc=1(phần tử đầu). So sánh2 > 1,acc=2.3 > 2,acc=3.4 > 3,acc=4. Kết quả4. Nếu array rỗng sẽ throw. Nên viếtnums.reduce((acc, n) => ..., -Infinity)hoặc dùngMath.max(...).
8. Implementation Lab (Bài lab thực hành)
Level 1 — Guided (Có hướng dẫn)
Viết function sumByCategory dùng reduce để tính tổng amount theo từng category.
const orders = [
{ category: "food", amount: 100 },
{ category: "electronics", amount: 200 },
{ category: "food", amount: 50 }
];
function sumByCategory(orders) {
return orders.reduce((acc, order) => {
// 1. Lấy category làm key
// 2. Nếu chưa có key, khởi tạo 0
// 3. Cộng amount vào
// 4. Return acc
}, {});
}
console.log(sumByCategory(orders));
// { food: 150, electronics: 200 }Gợi ý
acc[order.category] = (acc[order.category] || 0) + order.amount là cách ngắn gọn. Hoặc dùng if (!acc[key]) acc[key] = 0.
[Đáp án tham khảo]
function sumByCategory(orders) {
return orders.reduce((acc, order) => {
const key = order.category;
if (!acc[key]) {
acc[key] = 0;
}
acc[key] += order.amount;
return acc;
}, {});
}
const orders = [
{ category: "food", amount: 100 },
{ category: "electronics", amount: 200 },
{ category: "food", amount: 50 }
];
console.log(sumByCategory(orders));
// { food: 150, electronics: 200 }Giải thích:
- Initial value là
{}— object rỗng. - Mỗi bước kiểm tra
acc[key]đã tồn tại chưa. Nếu chưa, khởi tạo0. - Cộng
order.amountvào key tương ứng. - Bắt buộc return
acc— nếu không, lần sauacclàundefined.
Level 2 — Partial Scaffold (Khung sẵn)
Hoàn thành function indexById để biến array thành object lookup theo id.
function indexById(items) {
return items.reduce((acc, item) => {
// Gán item.id làm key, item làm value
// Return acc
}, ___);
}
const users = [
{ id: "u1", name: "Alice" },
{ id: "u2", name: "Bob" }
];
const lookup = indexById(users);
console.log(lookup["u1"].name); // "Alice"
console.log(lookup["u2"].name); // "Bob"[Đáp án tham khảo]
function indexById(items) {
return items.reduce((acc, item) => {
acc[item.id] = item;
return acc;
}, {});
}
const users = [
{ id: "u1", name: "Alice" },
{ id: "u2", name: "Bob" }
];
const lookup = indexById(users);
console.log(lookup["u1"].name); // "Alice"
console.log(lookup["u2"].name); // "Bob"Giải thích:
- Initial value
{}. acc[item.id] = itemtạo key-value map.- Trả về
accđể lan truyền. - Kết quả là object có thể tra cứu bằng
lookup[id]thay vìfindqua array — O(1) vs O(n).
Level 3 — Independent (Tự viết)
Viết các function sau bằng reduce (không dùng for loop):
// 1. countBy(array, key) → Đếm số lần xuất hiện của mỗi giá trị tại property `key`
// Ví dụ: countBy([{type: "A"}, {type: "B"}, {type: "A"}], "type") → { A: 2, B: 1 }
// 2. flatten(arrays) → "Dẹt" một cấp array của arrays
// Ví dụ: flatten([[1, 2], [3], [4, 5]]) → [1, 2, 3, 4, 5]
// Gợi ý: dùng spread trong accumulator
// 3. groupBy(array, key) → Nhóm object theo property `key`
// Ví dụ: groupBy([{cat: "X", v: 1}, {cat: "Y", v: 2}, {cat: "X", v: 3}], "cat")
// → { X: [{cat: "X", v: 1}, {cat: "X", v: 3}], Y: [{cat: "Y", v: 2}] }
// Sử dụng:
const logs = [
{ level: "error", msg: "A" },
{ level: "warn", msg: "B" },
{ level: "error", msg: "C" }
];
console.log(countBy(logs, "level")); // { error: 2, warn: 1 }
console.log(groupBy(logs, "level"));
// {
// error: [{ level: "error", msg: "A" }, { level: "error", msg: "C" }],
// warn: [{ level: "warn", msg: "B" }]
// }[Đáp án tham khảo]
function countBy(array, key) {
return array.reduce((acc, item) => {
const value = item[key];
acc[value] = (acc[value] || 0) + 1;
return acc;
}, {});
}
function flatten(arrays) {
return arrays.reduce((acc, arr) => {
return [...acc, ...arr];
}, []);
}
function groupBy(array, key) {
return array.reduce((acc, item) => {
const groupKey = item[key];
if (!acc[groupKey]) {
acc[groupKey] = [];
}
acc[groupKey].push(item);
return acc;
}, {});
}
const logs = [
{ level: "error", msg: "A" },
{ level: "warn", msg: "B" },
{ level: "error", msg: "C" }
];
console.log(countBy(logs, "level"));
// { error: 2, warn: 1 }
console.log(flatten([[1, 2], [3], [4, 5]]));
// [1, 2, 3, 4, 5]
console.log(groupBy(logs, "level"));
// {
// error: [
// { level: "error", msg: "A" },
// { level: "error", msg: "C" }
// ],
// warn: [
// { level: "warn", msg: "B" }
// ]
// }Giải thích:
countBy:(acc[value] || 0) + 1là pattern phổ biến — nếu chưa có thì tính là 0, cộng 1.flatten:[...acc, ...arr]tạo array mới mỗi bước. Lưu ý: với array lớn, đây là O(n²) vì copy lại mảng mỗi lần. Ở Stage 0 chấp nhận được; Stage 11 sẽ học cách tối ưu.groupBy: Tương tựsumByCategorynhưng value là array object thay vì số. Mutate accumulator array (push) là chấp nhận được vì{}là object ta tự tạo.
9. Edge Cases (Các trường hợp ngoại lệ)
Quên return trong callback
const nums = [1, 2, 3];
const sum = nums.reduce((acc, n) => { acc + n; }, 0);
// NaN (hoặc lỗi ở bước sau)Tại sao: Block body không có explicit return → undefined. undefined + n = NaN.
Cách nhận biết: Kết quả NaN, undefined, hoặc TypeError.
Cách xử lý: Dùng expression body acc + n hoặc thêm return.
Array rỗng không có initialValue
const empty = [];
const sum = empty.reduce((acc, n) => acc + n);
// TypeError: Reduce of empty array with no initial valueTại sao: Không có phần tử đầu để làm acc, cũng không có initial value.
Cách nhận biết: TypeError khi array có thể rỗng.
Cách xử lý: Luôn cung cấp initial value: reduce(..., 0).
Mutate accumulator object từ bên ngoài
const shared = {};
const a = [1, 2].reduce((acc, n) => { acc[n] = n; return acc; }, shared);
const b = [3, 4].reduce((acc, n) => { acc[n] = n; return acc; }, shared);
console.log(shared); // { 1: 1, 2: 2, 3: 3, 4: 4 }Tại sao: Cả hai reduce dùng chung shared làm initial value. Mutate accumulator = mutate shared.
Cách nhận biết: Object "tạm" bị thay đổi bất ngờ sau nhiều lần gọi.
Cách xử lý: Luôn dùng literal mới làm initial value: reduce(..., {}) thay vì reduce(..., shared).
reduce làm việc của map, filter, hoặc flat
// Anti-pattern: reduce thay map
const doubled = nums.reduce((acc, n) => {
acc.push(n * 2);
return acc;
}, []);
// Anti-pattern: reduce thay flat (đã có native method)
const flattened = arrays.reduce((acc, arr) => [...acc, ...arr], []);Tại sao: map và flat tồn tại để làm điều này. reduce dài hơn, dễ quên return, và nói sai intent. flat còn tối ưu hơn spread loop O(n²).
Cách nhận biết: Callback chỉ thêm vào accumulator array mà không "tích lũy" gì phức tạp (object map, grouped data, sum).
Cách xử lý: Dùng nums.map(n => n * 2) hoặc arrays.flat().
reduce với một phần tử và không có initialValue
const nums = [5];
const sum = nums.reduce((acc, n) => acc + n);
console.log(sum); // 5Tại sao: Không có initial value, acc = phần tử đầu (5), không có phần tử thứ hai để lặp. Callback không được gọi. Kết quả là phần tử duy nhất.
Cách nhận biết: Logic reduce không chạy như mong đợi với array 1 phần tử.
Cách xử lý: Luôn cung cấp initial value để callback luôn được gọi với mọi phần tử.
10. Debug Lab (Bài lab gỡ lỗi)
Symptom (Triệu chứng): Function countTypes trả về kết quả sai sau lần gọi thứ hai.
const initial = {};
function countTypes(items) {
return items.reduce((acc, item) => {
acc[item.type] = (acc[item.type] || 0) + 1;
return acc;
}, initial);
}
const batch1 = [{ type: "A" }, { type: "B" }];
const batch2 = [{ type: "A" }, { type: "C" }];
console.log(countTypes(batch1)); // { A: 1, B: 1 }
console.log(countTypes(batch2)); // { A: 2, B: 1, C: 1 } ❌Reproduction (Tái hiện lỗi): Chạy code trên.
Evidence (Bằng chứng):
- Lần gọi thứ hai có
B: 1dùbatch2không có type"B". Alà2thay vì1.
Hypothesis (Giả thuyết): initial là object được share giữa các lần gọi. Lần gọi đầu mutate initial thành {A: 1, B: 1}. Lần gọi hai dùng cùng object, tiếp tục cộng dồn.
Verification (Xác minh):
console.log(initial); // { A: 2, B: 1, C: 1 } — object bên ngoài bị sửa!Root Cause (Nguyên nhân gốc rễ): Dùng biến const initial = {} làm initial value cho reduce. reduce mutate accumulator (chấp nhận được nếu là object tạm), nhưng ở đây accumulator là object được share bên ngoài function. Đây là side effect ẩn.
Fix (Sửa):
function countTypes(items) {
return items.reduce((acc, item) => {
acc[item.type] = (acc[item.type] || 0) + 1;
return acc;
}, {}); // ← literal mới mỗi lần gọi
}Prevention (Phòng ngừa):
- Không bao giờ dùng object/array từ bên ngoài làm initial value của
reduce. - Nếu cần default object, dùng literal trong call:
reduce(..., {}). - Trong code review, kiểm tra argument thứ hai của
reduce.
11. Design Exercise (Bài tập thiết kế giải pháp)
Bạn cần lấy tên các user active từ một array.
const names = users
.filter(u => u.active)
.map(u => u.name);const names = users.reduce((acc, u) => {
if (u.active) acc.push(u.name);
return acc;
}, []);Câu hỏi:
- Option A đọc như thế nào về intent?
- Option B có lợi gì về performance?
- Với array 1,000 phần tử, bạn chọn cái nào? Tại sao?
[Đáp án tham khảo]
Bạn nghĩ:
- Option A đọc: "Lọc active, rồi lấy tên." Mỗi bước một verb rõ ràng. Dễ debug: có thể
.filter(...)xong inspect kết quả trước khi.map. - Option B chỉ lặp array một lần thay vì hai. Với array rất lớn, đỡ overhead một vòng lặp.
- Với 1,000 phần tử, chọn Option A. Lý do: readability > micro-performance. JavaScript engine tối ưu tốt, hai vòng 1,000 phần tử không đáng kể. Option B dài hơn, dễ quên
return, và nói sai intent (reduceđể accumulate, không phải filter+map).
- Option A đọc: "Lọc active, rồi lấy tên." Mỗi bước một verb rõ ràng. Dễ debug: có thể
Kết luận:
reducekhông phải replacement chofilter+map. Chỉ dùngreducekhi output thực sự là một giá trị tổng hợp (số, object map, grouped data) mà các method riêng lẻ không làm được.
12. Production Scenario (Tình huống thực tế)
Context: API trả về danh sách 5,000 sản phẩm. UI cần tra cứu sản phẩm theo ID nhanh.
const products = [
{ id: "p1", name: "Phone", price: 500 },
{ id: "p2", name: "Laptop", price: 1200 },
// ... 5000 items
];
// Cách 1: find — O(n) mỗi lần tra
const found = products.find(p => p.id === "p2999");
// Cách 2: reduce — O(n) một lần, O(1) mỗi lần tra
const productMap = products.reduce((acc, p) => {
acc[p.id] = p;
return acc;
}, {});
const foundFast = productMap["p2999"];Symptom: App chậm khi user chuyển nhanh giữa các sản phẩm. Profile cho thấy find được gọi hàng trăm lần, mỗi lần duyệt 5,000 phần tử.
Constraint: Không được thay đổi API response format.
Câu hỏi:
- Tại sao
findlặp đi lặp lại là vấn đề? reduceở đây giải quyết vấn đề gì?- Nếu
productscó thể cập nhật (thêm/xóa),productMapcần xử lý thế nào?
[Đáp án tham khảo]
Bạn nghĩ:
findduyệt tuyến tính O(n). Gọi 100 lần = 100 × 5,000 = 500,000 operations.reducexây object map một lần O(n). Sau đó tra cứu bằng key là O(1). 100 lần tra = 100 operations. Tổng cộng ~5,100 thay vì 500,000.- Nếu
productsthay đổi,productMapphải rebuild hoặc đồng bộ. Có thể wrap trong một functiongetProductByIdvới cache, hoặc dùng state management (Stage 8). Ở Stage 0, nhận biết trade-off: pre-computation bằngreducetốn bộ nhớ (lưu object map) để đổi lấy tốc độ tra cứu.
Bài học:
reduceđể biến array thành lookup structure là pattern production phổ biến. Nó giải quyết "array scan repeated" bằng "index once, query many".
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
reduceđể tính tổng nhưng thiếu initial value. Dự đoán kết quả với array có 1 phần tử và array rỗng.jsconst nums = [5]; const sum = nums.reduce((acc, n) => acc + n); console.log(sum); - Hỏi AI: "Tại sao kết quả là 5 chứ không phải NaN? Tôi không truyền initial value."
- So sánh câu trả lời AI với nhận định của bạn. AI có giải thích rõ callback không được gọi khi array chỉ có 1 phần tử và không có initial value?
- Verify bằng MDN: tìm
Array.prototype.reducetrên MDN và đọc phần "Description", tìm đoạn về "first element" và "no initial value".
Gợi ý
AI thường biết kết quả nhưng đôi khi giải thích mơ hồ bằng cách nói "nó lấy phần tử đầu làm giá trị khởi tạo". Nếu AI không chỉ ra rằng callback hoàn toàn không được thực thi trong trường hợp này — nghĩa là logic bên trong callback (validation, side effect, transform) bị bỏ qua — bạn đã tìm ra điểm mù. Điều này cực kỳ quan trọng trong production nếu callback có logic ngoài accumulation.
Đáp án tham khảo
Bạn nghĩ: Với
[5]và không có initial value,reducelấy5làmacc, không có phần tử thứ hai, nên callback không được gọi. Kết quả trả về5ngay lập tức. Nếu callback có logic quan trọng (ví dụ: validate input), logic đó bị skip.AI trả lời (typical):
- If no initial value is provided, the first element is used as the accumulator.
- Since there's only one element, the callback is not executed.
- The result is that single element.
So sánh: AI thường đúng về kết quả nhưng đôi khi không nhấn mạnh hậu quả production: nếu callback của bạn không chỉ
acc + nmà còn kiểm tra điều kiện, log, hoặc transform, toàn bộ logic đó bị bỏ qua với array 1 phần tử. Đây là silent behavior change.Điểm AI nói sai hoặc quá mơ hồ: "The first element is used as the accumulator" — đúng nhưng chưa đủ. AI hiếm khi nói rõ: "Điều này có nghĩa là code bên trong callback của bạn không chạy. Nếu bạn dựa vào callback để validate hoặc side effect, bạn có bug với single-element array."
Kết luận: Nếu bạn chỉ ra được rằng thiếu initial value không chỉ là "style issue" mà còn là correctness risk (callback skip), bạn đã hiểu sâu hơn AI. Concept này sẽ quay lại ở Stage 8 (React — reducer pattern luôn cần initial state) và Stage 12 (Architecture — state machine initialization).
14. Teach Back (Dạy lại)
Giả sử một junior developer hỏi bạn:
"Em thấy
reducekhó hiểu quá. Người ta bảo nó 'gom array thành một giá trị' nhưngmapcũng trả về một giá trị (array mà). Và tại sao em phảireturntrong callback củareduce? TrongforEachem không cần mà?"
Hãy giải thích trong 2 phút, dùng đúng terminology: accumulator, aggregation, return value propagation.
Mô phỏng
- Bạn nói:
maptrả về array, nhưng nó làm việc với từng phần tử riêng lẻ — biến A thành A', B thành B'.reducelàm việc với toàn bộ collection — gom A, B, C thành một thứ duy nhất, ví dụ tổng của chúng.
Về return: reduce là một chuỗi. Bước 1 tính ra kết quả tạm, truyền cho bước 2. Bước 2 dùng kết quả đó + phần tử hiện tại, tính ra kết quả mới, truyền cho bước 3. Nếu bạn không return, bước sau nhận undefined — chuỗi đứt. forEach không cần return vì nó không truyền gì cho lần sau; mỗi lần chạy độc lập.
Ví dụ: Tính tổng tiền giỏ hàng. map là "tính giá từng sản phẩm". reduce là "cộng tất cả giá lại thành tổng cuối".
💡 Tưởng tượng
reducenhư một trò chuyền bóng. Bạn đứng đầu hàng, nhận quả bóng (initial value), nhìn sản phẩm đầu tiên, viết gì đó lên bóng, rồi chuyền cho người tiếp theo. Người cuối cùng đưa bóng cho coach — đó là kết quả. Nếu bạn không chuyền bóng đi (quên return), trò chơi dừng lại.
Gợi ý đánh giá bản thân
- Đồng nghiệp có hiểu tại sao
reducecần return cònforEachkhông? - Bạn có tránh được việc nói "
reducechỉ để tính tổng" không? (Nó còn để nhóm, index, flatten, concat...) - Nếu đồng nghiệp hỏi: "Vậy khi nào em không dùng
reduce?" — bạn trả lời được không? (Gợi ý: khimap/filter/findđã đủ; khi code reduce dài hơn 3 dòng và không rõ intent.) - Nếu đồng nghiệp hỏi: "Tại sao phải truyền
0vàoreduce(..., 0)?" — bạn giải thích được callback skip và empty array bug trong 30 giây không?
15. Assessment (Đánh giá)
| Objective (Mục tiêu) | Hình thức đánh giá | Task |
|---|---|---|
| Explain reduce mental model (Giải thích mental model) | Teach Back | Teach Back Section |
| Predict without initialValue (Dự đoán thiếu initialValue) | Prediction (Dự đoán) | Prediction Câu 1, 2, 5 |
| Predict missing return (Dự đoán quên return) | Prediction (Dự đoán) | Prediction Câu 3 |
| Implement sum/count/group/indexBy | Implementation (Thực hành) | Implementation Lab Level 1–3 |
| Avoid shared accumulator bug | Debug Lab | Debug Lab Section |
| Choose reduce vs map/filter | Design Decision | Design Exercise |
| Build lookup map for performance | Production Scenario | Production Scenario |
16. Exit Criteria (Tiêu chí qua bài)
- [ ] Có thể giải thích mental model của
reduce: accumulator được truyền từ bước này sang bước khác, final result là accumulator cuối cùng. - [ ] Có thể viết
reduceđể tính tổng, đếm, nhóm, và index-by-ID từ array. - [ ] Có thể dự đoán behavior khi thiếu
initialValue(dùng phần tử đầu, callback không chạy với array 1 phần tử, throw với array rỗng). - [ ] Có thể dự đoán behavior khi callback quên
return(undefinedlan truyền). - [ ] Có thể tránh dùng shared object làm initial value (mutate bug).
- [ ] Có thể nhận diện khi
reducelà overkill so vớimap/filterchain. - [ ] Có thể dùng
reduceđể xây lookup map từ array, giải thích trade-off bộ nhớ vs tốc độ tra cứu.
17. Spiral Connection (Liên kết xoắn ốc)
Previous (Trước): Array Iteration (0.6.3) — bạn đã biết
map,filter,find. Giờ bạn học cách tổng hợp toàn bộ array thành một giá trị duy nhất bằngreduce.
Current (Hiện tại): Reduce — aggregation mental model (accumulator → current → next accumulator → result). Sum, count, group, index. Đúng và sai khi dùng
reduce.
Next (Tiếp theo):
- 0.6.5 (Objects) — Object iteration:
Object.keys,values,entries. Kết hợp vớireduceđể transform object.- 0.6.8 (Spread & Shallow Copy) — Trong
reducecallback, mutate accumulator object là chấp nhận được nếu object được tạo mới ({}). Nhưng nếu accumulator chứa nested object, cần shallow/deep copy awareness.- Stage 1 (Execution Model) — Callback trong
reducetạo scope mới mỗi lần gọi. Hiểu execution context giúp debug closure trong reduce.- Stage 8 (React) —
useReducerhook dùng chính reduce pattern:(state, action) => newState. Accumulator là state, current value là action. Initial value là initial state. Quên return = state mất.- Stage 12 (Architecture) — Reducer pattern ở cấp hệ thống (Redux, state machines): pure function nhận state + action → new state. Không mutate state trực tiếp.