JavaScript
Array Methods
reduce
เรียนรู้ reduce() — method รวมทุก element ใน array ให้เป็นค่าเดียวผ่าน accumulator ตั้งแต่ผลรวมตัวเลข หาค่าสูงสุด นับจำนวน จัดกลุ่มข้อมูล แปลง array เป็น object ข้อผิดพลาดที่พบบ่อย ไปจนถึง reduce() vs for loop ผ่าน Lab 3 ข้อ
`reduce()` คืออะไร — รวมทุก element ให้เป็นค่าเดียว
`reduce()` เป็น method ของ array ที่รับ callback function แล้ววน element ทุกตัว — แต่ต่างจาก `map()`, `filter()`, `find()`, `some()`, `every()` ตรงที่ **`reduce()` คืนค่าเพียงค่าเดียว** (ไม่จำเป็นต้องเป็น array) หัวใจของ `reduce()` คือ **accumulator (ตัวสะสม)** — ค่าที่วิ่งผ่านทุกรอบ โดย callback จะ return accumulator ใหม่ออกมาในแต่ละรอบ: ``` array.reduce((accumulator, element, index, array) => { // คำนวณ accumulator ใหม่ return accumulator ใหม่; }, initialValue); ``` กฎสำคัญ: • `reduce()` ต้อง return ค่าใหม่สำหรับ accumulator ใน callback เสมอ • `initialValue` คือค่าเริ่มต้นของ accumulator — รอบแรก accumulator = initialValue • ถ้าไม่กำหนด `initialValue` — `reduce()` จะใช้ `element[0]` เป็น accumulator แทน • `reduce()` ไม่เปลี่ยน array ต้นฉบับ (immutable method) ใช้ `reduce()` เมื่อคุณต้องการ "รวม" array ให้เป็นค่าเดียว — ผลรวม ผลลัพธ์สะสม ข้อมูลที่จัดกลุ่มใหม่ หรือแปลง array เป็น object
const nums = [1, 2, 3, 4, 5];
// reduce() รับ callback + initialValue = 0
const total = nums.reduce((acc, n) => acc + n, 0);
console.log(total); // 15
// รอบ 1: acc = 0, n = 1 → return 1
// รอบ 2: acc = 1, n = 2 → return 3
// รอบ 3: acc = 3, n = 3 → return 6
// รอบ 4: acc = 6, n = 4 → return 10
// รอบ 5: acc = 10, n = 5 → return 15
console.log(nums); // [1, 2, 3, 4, 5] ← ต้นฉบับไม่เปลี่ยน ✓
สิ่งที่ต้องรู้จากตัวอย่าง: • `acc` คือ accumulator — เริ่มต้นจาก 0 (initialValue) — แต่ละรอบ `acc` คือค่าที่ return จากรอบก่อนหน้า • `n` คือ element ปัจจุบัน — 1, 2, 3, 4, 5 ตามลำดับ • callback return `acc + n` — ค่าใหม่ของ accumulator ในแต่ละรอบ • รอบสุดท้าย return 15 — `reduce()` คืน 15 เป็นผลลัพธ์สุดท้าย คิดแบบนี้: `reduce()` เหมือนกำลังถือกล่องเดินไปตาม element แต่ละตัว — หยิบของใส่กล่อง แล้วส่งกล่องต่อไป — รอบสุดท้ายคุณจะได้กล่องที่บรรจุทุกอย่าง
reduce() เริ่มจาก initialValue → วน element ทุกตัว → accumulator รับค่าใหม่จาก callback ในแต่ละรอบ → คืนค่าสุดท้ายเพียงค่าเดียว
Accumulator กับ initialValue — กลไกสำคัญที่สุดของ `reduce()`
**initialValue** กำหนดทั้งค่าเริ่มต้นของ accumulator และ **ประเภทของผลลัพธ์** ``` // กำหนด 0 → ผลลัพธ์เป็น number (ผลรวม) // กำหนด "" → ผลลัพธ์เป็น string (ต่อข้อความ) // กำหนด [] → ผลลัพธ์เป็น array (เก็บค่าที่ผ่านเงื่อนไข) // กำหนด {} → ผลลัพธ์เป็น object (จัดกลุ่มข้อมูล) ``` initialValue ควรกำหนดเสมอ — เพราะ: • ทำให้ accumulator มีประเภทที่แน่นอนตั้งแต่รอบแรก • หลีกเลี่ยง TypeError เมื่อ array ว่าง • ทำให้อ่านโค้ดแล้วเข้าใจเจตนาทันที
const nums = [1, 2, 3, 4, 5];
// === มี initialValue = 0 → รอบแรก acc = 0, n = 1
const withInit = nums.reduce((acc, n) => acc + n, 0);
console.log(withInit); // 15
// === ไม่มี initialValue → รอบแรก acc = 1 (element[0]), n = 2
const withoutInit = nums.reduce((acc, n) => acc + n);
console.log(withoutInit); // 15 — ผลลัพธ์เหมือนกัน! แต่..
// รอบ 1: acc = 1, n = 2 → return 3
// รอบ 2: acc = 3, n = 3 → return 6
// ... วนแค่ 4 รอบ (ไม่ใช่ 5)
// === ⚠️ ไม่มี initialValue + array ว่าง → TypeError!
const empty = [];
// console.log(empty.reduce((acc, n) => acc + n)); // ❌ TypeError!
// ✅ มี initialValue → ปลอดภัยแม้ array ว่าง
console.log(empty.reduce((acc, n) => acc + n, 0)); // 0 ✓
จากตัวอย่าง — เห็นว่า: • **มี initialValue**: callback ถูกเรียก N รอบ (เท่ากับจำนวน element) — รอบแรก `acc = initialValue` • **ไม่มี initialValue**: callback ถูกเรียก N−1 รอบ — รอบแรก `acc = element[0]`, เริ่มวนที่ `element[1]` • **array ว่าง + ไม่มี initialValue** → `TypeError: Reduce of empty array with no initial value` เคล็ดลับ: กำหนด `initialValue` เสมอ — ยกเว้นเมื่อมั่นใจ 100% ว่า array ไม่มีทางว่าง และผลรวมเริ่มจาก element แรกได้
| เรื่อง | reduce() | map() | filter() |
|---|---|---|---|
| ผลลัพธ์ | ค่าเดียว (any type) | Array ใหม่ (ขนาดเท่าเดิม) | Array ใหม่ (ขนาด ≤ เดิม) |
| callback ต้อง return | ค่าใหม่ของ accumulator | element ใหม่ | boolean (true/false) |
| ใช้เมื่อ | รวม array → ค่าเดียว | แปลงทุก element | กรองบาง element |
| เปลี่ยนต้นฉบับ | ไม่เปลี่ยน ✓ | ไม่เปลี่ยน ✓ | ไม่เปลี่ยน ✓ |
| มี accumulator | ใช่ — หัวใจของ reduce | ไม่ใช่ | ไม่ใช่ |
`reduce()` ในสถานการณ์จริง — มากกว่าแค่ผลรวม
`reduce()` ทรงพลังมาก — ใช้ทำงานได้หลากหลายกว่าผลรวมตัวเลข: • **หาค่าสูงสุด / ต่ำสุด** — เปรียบเทียบ accumulator กับ element ในแต่ละรอบ • **นับจำนวน element ที่ตรงเงื่อนไข** — accumulator สะสม count • **จัดกลุ่มข้อมูล (group by)** — แปลง array → object ที่จัดกลุ่มตาม property • **รวม object** — merge array ของ object ให้เป็น object เดียว • **แปลง array → object** — สร้าง lookup table, key-value map • **chain กับ map/filter** — ใช้ map/filter เตรียมข้อมูล → reduce สรุปผล
const scores = [78, 85, 62, 91, 73, 88];
// ใช้ reduce() หาค่าสูงสุด — เปรียบเทียบ acc กับ element
const highest = scores.reduce((acc, n) => n > acc ? n : acc, scores[0]);
console.log(highest); // 91
// รอบ 1: acc = 78, n = 85 → 85 > 78 → return 85
// รอบ 2: acc = 85, n = 62 → 62 > 85 → return 85
// รอบ 3: acc = 85, n = 91 → 91 > 85 → return 91
// ...
// 91 คือค่าสูงสุด — reduce() จำค่าสูงสุดไว้ใน accumulator
const students = [
{ name: "สมชาย", score: 85 },
{ name: "สมหญิง", score: 62 },
{ name: "สมศรี", score: 91 },
{ name: "สมบัติ", score: 55 },
{ name: "สมหมาย", score: 78 },
];
// นับจำนวนคนที่สอบผ่าน (score >= 70)
const passedCount = students.reduce(
(count, s) => s.score >= 70 ? count + 1 : count,
0
);
console.log(passedCount); // 4
// acc เริ่มที่ 0 — ถ้า score >= 70 → +1 — ถ้าไม่ผ่าน → เท่าเดิม
// สมชาย 85 → 1, สมหญิง 62 → 1, สมศรี 91 → 2, สมบัติ 55 → 2, สมหมาย 78 → 3
// เปรียบเทียบกับ filter() — มักทำได้สั้นกว่า
console.log(students.filter(s => s.score >= 70).length); // 4 — สั้นกว่า!
// reduce มีข้อดีเมื่อต้องนับหลายอย่างพร้อมกันในรอบเดียว
const products = [
{ name: "ปลาทู", category: "อาหาร" },
{ name: "น้ำเปล่า", category: "เครื่องดื่ม" },
{ name: "กะเพรา", category: "อาหาร" },
{ name: "ชาเขียว", category: "เครื่องดื่ม" },
{ name: "ไข่ดาว", category: "อาหาร" },
];
// ใช้ reduce() จัดกลุ่มสินค้าตาม category
const grouped = products.reduce((acc, p) => {
// ถ้ายังไม่มี key นี้ → สร้าง array เปล่า
if (!acc[p.category]) {
acc[p.category] = [];
}
acc[p.category].push(p.name);
return acc; // ⚠️ ต้อง return acc เสมอ!
}, {});
console.log(grouped);
// {
// อาหาร: ['ปลาทู', 'กะเพรา', 'ไข่ดาว'],
// เครื่องดื่ม: ['น้ำเปล่า', 'ชาเขียว']
// }
// เริ่มด้วย {} (object) — วน element แต่ละตัว — เติม key → return object
สังเกตรูปแบบที่ใช้บ่อย: • `reduce((acc, n) => n > acc ? n : acc)` — จับค่าสูงสุด (max) • `reduce((count, x) => cond ? count + 1 : count, 0)` — นับ • `reduce((acc, x) => { ...; return acc; }, {})` — สร้าง object จุดร่วมของทุก use case: **ต้อง `return acc` ใหม่ใน callback เสมอ** — `reduce()` เอา return value มาเป็น accumulator ในรอบถัดไป
ข้อผิดพลาดที่พบบ่อยเมื่อใช้ `reduce()`
- ลืม `return acc` ใน callback — `reduce((acc, n) => { acc += n })` — ไม่มี return → callback return `undefined` → รอบถัดไป `acc = undefined` → `undefined + n = NaN` หรือ TypeError — แก้โดยเติม `return acc` หรือใช้ arrow shorthand `(acc, n) => acc + n`
- ลืมกำหนด initialValue — `[].reduce((acc, n) => acc + n)` → TypeError — array ว่าง + ไม่มี initialValue = error — กำหนด initialValue เสมอเมื่อ array อาจว่างได้ — `[].reduce((acc, n) => acc + n, 0)` → 0 ✓
- กำหนด initialValue ผิดประเภท — `['a','b','c'].reduce((acc, s) => acc + s.length, 0)` → `0 + 1 + 2 + 3 = 6` ✓ — แต่ถ้าเป็น `['a','b','c'].reduce((acc, s) => acc + s, '')` → `'0123'` — initialValue กำหนดประเภทของผลลัพธ์ — เลือกให้ตรงกับสิ่งที่อยากได้
- ใช้ `reduce()` เมื่อไม่ต้องการ accumulator — ถ้าต้องการแค่ "ทำอะไรกับทุกตัว" โดยไม่ต้อง return ค่าสะสม → ใช้ `forEach()` หรือ `for...of` — `reduce()` มีไว้เพื่อ "รวม" ไม่ใช่ "วน loop ทำอะไร"
- แก้ไข accumulator โดยไม่ return — `reduce((acc, n) => acc.push(n), [])` → `push()` return length (number) — รอบถัดไป `acc` เป็น number → `.push()` error! — ต้องแยก mutation กับ return: `reduce((acc, n) => { acc.push(n); return acc; }, [])` — หรือใช้ shorthand `(acc, n) => [...acc, n]`
const nums = [1, 2, 3, 4, 5];
// ❌ ผิด — ลืม return (ใช้ { } ใน arrow function)
const wrong = nums.reduce((acc, n) => {
acc += n; // ← ไม่มี return! callback return undefined
});
console.log(wrong); // undefined — ผิด!
// ❌ ผิด — .push() return length ไม่ใช่ array
const wrong2 = nums.reduce((acc, n) => acc.push(n), []);
// push() return 1, 2, 3, ... → acc กลายเป็น number → .push() error!
// ✅ ถูก — return acc อย่างชัดเจน
const correct = nums.reduce((acc, n) => {
acc.push(n);
return acc; // ← ต้อง return!
}, []);
console.log(correct); // [1, 2, 3, 4, 5] ✓
// ✅ ถูก — ใช้ arrow shorthand (return นัยโดยตรง)
const total = nums.reduce((acc, n) => acc + n, 0);
console.log(total); // 15 ✓
// ✅ ถูก — ใช้ concat() + return นัย
const copied = nums.reduce((acc, n) => acc.concat([n]), []);
console.log(copied); // [1, 2, 3, 4, 5] ✓
const names = ["สมชาย", "สมหญิง", "สมศรี"];
// ❌ ใช้ reduce() เพื่อ console.log — แค่ทำอะไร ไม่ได้รวมค่า
names.reduce((_, n) => {
console.log("Hello " + n); // แค่พิมพ์ ไม่ได้สะสมอะไร
}, null);
// ใช้ reduce() ผิดวัตถุประสงค์ — ไม่ได้ "รวม" อะไรเลย
// ✅ ใช้ forEach() — สำหรับทำอะไรกับทุกตัวโดยไม่ต้อง return ค่าสะสม
names.forEach(n => console.log("Hello " + n));
// ✅ ใช้ reduce() เมื่อต้องการ "รวม" จริง ๆ
const greeting = names.reduce((acc, n) => acc + " " + n, "Hello");
console.log(greeting); // "Hello สมชาย สมหญิง สมศรี"
`reduce()` vs วิธีอื่น — เลือกใช้ให้เหมาะ
งานเดียวกันทำได้หลายวิธี — `reduce()`, `for` loop, `for...of` — แต่แนวคิดต่างกัน: • **`reduce()`** — บอกผลลัพธ์ที่อยากได้ (declarative — "เอาทุกตัวมารวมกันด้วย +") • **`for` loop** — บอกขั้นตอนทีละขั้น (imperative — "เริ่มที่ 0, เอามาบวก, i++...") • **`for...of`** — อ่านง่ายกว่า `for` loop แต่ยังต้องประกาศตัวแปร accumulator เอง `reduce()` สั้นและสื่อเจตนาชัด — อ่าน `reduce(...)` แล้วรู้ทันทีว่า "กำลังรวมข้อมูล"
| เรื่อง | reduce() | for loop | for...of |
|---|---|---|---|
| จำนวนบรรทัด | 1–3 บรรทัด | 4–5 บรรทัด | 3–4 บรรทัด |
| ต้องประกาศ accumulator | ไม่ต้อง — ใช้ initialValue | ต้องประกาศเอง | ต้องประกาศเอง |
| ต้องจัดการ index | ไม่ต้อง (เว้นแต่ใช้ index) | ต้องจัดการเอง | ไม่ต้อง |
| สื่อเจตนา | ชัด — "รวมข้อมูล" | ต้องไล่อ่าน | ต้องไล่อ่าน |
| เปลี่ยนต้นฉบับ | ไม่เปลี่ยน ✓ | ไม่เปลี่ยน (ถ้าใช้ตัวแปรใหม่) | ไม่เปลี่ยน (ถ้าใช้ตัวแปรใหม่) |
| เมื่อไหร่ควรใช้ | รวม array → ค่าเดียว | มีเงื่อนไขซับซ้อนมาก | ต้องการ break/continue |
const nums = [1, 2, 3, 4, 5];
// === ใช้ reduce() — 1 บรรทัด
const sum1 = nums.reduce((acc, n) => acc + n, 0);
// === ใช้ for loop — 4 บรรทัด
let sum2 = 0;
for (let i = 0; i < nums.length; i++) {
sum2 += nums[i];
}
// === ใช้ for...of — 3 บรรทัด
let sum3 = 0;
for (const n of nums) {
sum3 += n;
}
console.log(sum1); // 15
console.log(sum2); // 15
console.log(sum3); // 15
// ทั้งสามให้ผลลัพธ์เหมือนกัน — แต่ reduce() สั้นและสื่อเจตนาชัดที่สุด
เคล็ดลับในการเลือกใช้: • ถ้าโจทย์คือ "รวมทุก element ให้เป็นค่าเดียว" → ใช้ `reduce()` • ถ้าโจทย์คือ "ทำอะไรกับทุก element แต่ไม่ต้องการค่าสะสม" → ใช้ `forEach()` หรือ `for...of` • ถ้าโจทย์คือ "วน loop แบบต้อง break/continue" → ใช้ `for` หรือ `for...of` • ถ้าโจทย์คือ "แปลงทุก element" → ใช้ `map()` — ไม่ใช่ `reduce()` • อย่าใช้ `reduce()` เพียงเพราะมัน "เท่" — ใช้เมื่อมันทำให้โค้ดอ่านง่ายขึ้นจริง ๆ `reduce()` มีพลังมาก — ใช้มันได้ทั้ง `map()` และ `filter()` ในตัว — แต่โค้ดที่ดีคือโค้ดที่อ่านง่าย ไม่ใช่โค้ดที่สั้นที่สุด — ถ้า `map()` + `filter()` อ่านเข้าใจง่ายกว่า `reduce()` ตัวเดียว → ใช้ `map()` + `filter()` แทน