Skip to content

Lesson 0.6.4 — Reduce ​

Bài 0.6.4 — Reduce
Accumulator, Current value & Các pattern thực tế • 33 phút
0:00 / 0:00

0. Metadata ​

FieldValue
Stage0 — JavaScript Language Foundation
Module0.6 — Data Structures
Lesson0.6.4
CompetencyC01.6 — Arrays / Objects
Depth TargetL2–L3
PrerequisitesArray Iteration (0.6.3), Arrays (0.6.2), Callback (0.5.5)
Estimated Cognitive LoadMedium–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:

js
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, forEach và 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 const và 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ể:

  1. Giải thích mental model của reduce: accumulator, current value, next accumulator, final result.
  2. Viết reduce để tính tổng, đếm, nhóm, và index-by-ID.
  3. Phân biệt khi nào dùng reduce (aggregation) và khi nào dùng map/filter (transform/select).
  4. Dự đoán kết quả khi thiếu initialValue hoặc quên return trong callback.
  5. 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.
  6. Nhận diện khi reduce bị overkill (ví dụ: dùng reduce thay cho map).

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ền

Cấu trúc callback:

js
array.reduce((accumulator, currentValue, index, array) => {
  return nextAccumulator;
}, initialValue);

Quy tắc vàng:

  • reduce không dùng để "loop và làm gì đó" — đó là forEach.
  • reduce không dùng để "biến đổi từng phần tử thành array" — đó là map.
  • reduce dù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ắc filter trướ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.
AccumulatorGiá trị "tích lũy" được truyền từ callback này sang callback tiếp theo.
Current ValuePhần tử array đang xử lý.
Initial ValueGiá 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 callbackBắt buộc. Callback phải trả về next accumulator. Quên return → undefined lan truyền.

Supporting (Hỗ trợ) ​

ConceptÝ nghĩa
reduceRightGiống reduce nhưng duyệt từ phải sang trái. Dùng cho string operations hoặc right-associative.
Chainingfilter → map thường đọc rõ hơn reduce làm cả hai.

Awareness (Biết tồn tại) ​

ConceptLý do chưa đào sâu
Functional fold/foldlreduce là implementation của fold trong FP. Theory sâu hơn là Elective.
Async reducePromise.all + reduce pattern. Thuộc Stage 3 (Async).

Out of Scope (Không thuộc bài này) ​

  • Dùng reduce thay cho map/filter (anti-pattern, sẽ đề cập trong Design Exercise).
  • reduce vớ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.

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

  1. Tổng doanh thu.
  2. Số lượng đơn hàng theo danh mục: { food: 2, electronics: 2 }.
js
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 }
js
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} → return 100.
  • Step 2: sum = 100, order = {amount: 200} → return 300.
  • Step 3: sum = 300, order = {amount: 50} → return 350.
  • Step 4: sum = 350, order = {amount: 300} → return 650.
  • 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, return counts.
  • 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 ​

js
const nums = [1, 2, 3];
const sum = nums.reduce((acc, n) => acc + n);
console.log(sum);

Câu 2 ​

js
const nums = [];
const sum = nums.reduce((acc, n) => acc + n);
console.log(sum);

Câu 3 ​

js
const nums = [1, 2, 3];
const result = nums.reduce((acc, n) => {
  acc.push(n * 2);
}, []);
console.log(result);

Câu 4 ​

js
const items = ["a", "b", "c"];
const indexed = items.reduce((acc, item, index) => {
  acc[item] = index;
  return acc;
}, {});
console.log(indexed);

Câu 5 ​

js
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ếu return → trả về undefined. Lần thứ hai acc là undefined, acc.push lỗi. Đây là lỗi #1 của reduce.

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 property item: index. Return acc. 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ánh 2 > 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ết nums.reduce((acc, n) => ..., -Infinity) hoặc dùng Math.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.

js
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]
js
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ạo 0.
  • Cộng order.amount vào key tương ứng.
  • Bắt buộc return acc — nếu không, lần sau acc là undefined.

Level 2 — Partial Scaffold (Khung sẵn) ​

Hoàn thành function indexById để biến array thành object lookup theo id.

js
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]
js
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] = item tạ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ì find qua 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):

js
// 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]
js
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) + 1 là 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ự sumByCategory như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

js
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

js
const empty = [];
const sum = empty.reduce((acc, n) => acc + n);
// TypeError: Reduce of empty array with no initial value

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

js
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

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

js
const nums = [5];
const sum = nums.reduce((acc, n) => acc + n);
console.log(sum); // 5

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

js
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: 1 dù batch2 không có type "B".
  • A là 2 thay 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):

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

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

js
const names = users
  .filter(u => u.active)
  .map(u => u.name);
js
const names = users.reduce((acc, u) => {
  if (u.active) acc.push(u.name);
  return acc;
}, []);

Câu hỏi:

  1. Option A đọc như thế nào về intent?
  2. Option B có lợi gì về performance?
  3. 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ĩ:

    1. 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.
    2. 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.
    3. 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).
  • Kết luận: reduce không phải replacement cho filter + map. Chỉ dùng reduce khi 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.

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

  1. Tại sao find lặp đi lặp lại là vấn đề?
  2. reduce ở đây giải quyết vấn đề gì?
  3. Nếu products có thể cập nhật (thêm/xóa), productMap cần xử lý thế nào?
[Đáp án tham khảo]
  • Bạn nghĩ:

    1. find duyệt tuyến tính O(n). Gọi 100 lần = 100 × 5,000 = 500,000 operations.
    2. reduce xâ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.
    3. Nếu products thay đổi, productMap phải rebuild hoặc đồng bộ. Có thể wrap trong một function getProductById với cache, hoặc dùng state management (Stage 8). Ở Stage 0, nhận biết trade-off: pre-computation bằng reduce tố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

  1. 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.
    js
    const nums = [5];
    const sum = nums.reduce((acc, n) => acc + n);
    console.log(sum);
  2. 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."
  3. 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?
  4. Verify bằng MDN: tìm Array.prototype.reduce trê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, reduce lấy 5 làm acc, không có phần tử thứ hai, nên callback không được gọi. Kết quả trả về 5 ngay 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 + n mà 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 reduce khó hiểu quá. Người ta bảo nó 'gom array thành một giá trị' nhưng map cũng trả về một giá trị (array mà). Và tại sao em phải return trong callback của reduce? Trong forEach em 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: map trả 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'. reduce là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 reduce như 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 reduce cần return còn forEach không?
  • Bạn có tránh được việc nói "reduce chỉ để 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 ý: khi map/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 0 vào reduce(..., 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 BackTeach 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/indexByImplementation (Thực hành)Implementation Lab Level 1–3
Avoid shared accumulator bugDebug LabDebug Lab Section
Choose reduce vs map/filterDesign DecisionDesign Exercise
Build lookup map for performanceProduction ScenarioProduction 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 (undefined lan truyền).
  • [ ] Có thể tránh dùng shared object làm initial value (mutate bug).
  • [ ] Có thể nhận diện khi reduce là overkill so với map/filter chain.
  • [ ] 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ằng reduce.

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ới reduce để transform object.
  • 0.6.8 (Spread & Shallow Copy) — Trong reduce callback, 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 reduce tạo scope mới mỗi lần gọi. Hiểu execution context giúp debug closure trong reduce.
  • Stage 8 (React) — useReducer hook 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.
📴 Offline Mode — Content served from cache