JavaScript
Built-in Classes
Iterable
เข้าใจ iterable protocol — `Symbol.iterator`, iterator กับ `next()`, และการสร้าง custom iterable ที่ใช้งานได้จริง
Iterable Protocol: `Symbol.iterator` — กฎที่ทำให้ object ใช้กับ `for...of` และ spread ได้
คุณใช้ `for...of` กับ array และ string มาหลายบทแล้ว (บทที่ 66) และรู้ว่า spread ใช้กับ "iterable ใด ๆ" (บทที่ 77) — แต่วันนี้เราจะลงลึกว่าเบื้องหลังของคำว่า iterable คืออะไร **Iterable protocol** คือกฎข้อตกลงที่ JavaScript ใช้ตัดสินว่า object ไหนใช้กับ `for...of` หรือ spread ได้บ้าง กฎมีข้อเดียวคือ: > object นั้นต้องมี method ชื่อ `[Symbol.iterator]()` — และ method นี้ต้องคืนค่าเป็น **iterator object** object ไหนมี `[Symbol.iterator]()` — มันคือ **iterable** เมื่อคุณเขียน `for (const item of someObj)` — JavaScript จะทำงานให้คุณโดยอัตโนมัติ: 1. เรียก `someObj[Symbol.iterator]()` → ได้ iterator 2. เรียก `iterator.next()` ซ้ำ จน `done` เป็น `true` 3. ทุกครั้งที่เรียก — `value` จะถูกผูกเข้ากับ `item` ใน loop body นี่คือเหตุผลที่ array และ string ใช้กับ `for...of` ได้ — มัน implement `Symbol.iterator` มาให้แล้ว ส่วน plain object `{name: "Mali"}` ใช้กับ `for...of` ไม่ได้ — เพราะมันไม่มี `Symbol.iterator`
// === array — มี Symbol.iterator ===
const arr = [1, 2, 3];
console.log(typeof arr[Symbol.iterator]); // "function" ← มี → เป็น iterable
// === string — มี Symbol.iterator ===
const str = "AB";
console.log(typeof str[Symbol.iterator]); // "function" ← มี → เป็น iterable
// === plain object — ไม่มี Symbol.iterator ===
const obj = { name: "Mali", age: 25 };
console.log(typeof obj[Symbol.iterator]); // "undefined" ← ไม่มี → ไม่ใช่ iterable
// for...of ใช้กับ object ไม่ได้ — TypeError: obj is not iterable
// for (const val of obj) {} // ❌ error
// === วิธีวน key ของ object — แปลงเป็น array ก่อน ===
for (const key of Object.keys(obj)) {
console.log(key, obj[key]); // name Mali, age 25
}
// Object.keys(obj) คืน array ← array เป็น iterable → ใช้ for...of ได้- iterable คือ object ที่มี method `[Symbol.iterator]()` — method นี้ต้องคืน iterator
- เบื้องหลัง `for...of`, spread (`...`), และ destructuring (`[a, b] = iterable`) — ทั้งหมดเรียก `Symbol.iterator` ให้คุณ
- เช็กว่า object เป็น iterable ไหม: `typeof obj[Symbol.iterator] === "function"`
- plain object `{}` ไม่ใช่ iterable — ต้องแปลงเป็น array ด้วย `Object.keys()` / `Object.values()` / `Object.entries()` ก่อน
Iterator Protocol: `next()` คืน `{value, done}` — กลไกที่ก้าวผ่านค่าทีละตัว
เมื่อ `Symbol.iterator` ถูกเรียก — มันต้องคืน **iterator object** — แต่ iterator คืออะไร? **Iterator protocol** คือกฎว่า iterator ต้องเป็น object ที่มี method `next()` — และ `next()` ต้องคืน object ที่มี properties: - `value` — ค่าถัดไปในลำดับ (หรือ `undefined` ถ้าถึงจุดสิ้นสุดแล้ว) - `done` — `boolean` บอกว่ายังมีค่าที่จะวนต่อหรือยัง (`false` = ยังมีต่อ, `true` = จบแล้ว) ทุกครั้งที่ `for...of` วนหนึ่งรอบ — มันเรียก `iterator.next()` หนึ่งครั้ง → ได้ `{value, done}` → ถ้า `done` เป็น `false` ใช้ `value` → ถ้า `done` เป็น `true` หยุดวน การเรียก `next()` ด้วยตัวเองช่วยให้คุณเห็นกลไกที่ `for...of` ทำอัตโนมัติ — เหมือนเปิดฝาดูเครื่องยนต์
const arr = ["A", "B", "C"];
// 1. ได้ iterator
const iter = arr[Symbol.iterator]();
// 2. เรียก next() ทีละครั้ง
console.log(iter.next()); // { value: "A", done: false }
console.log(iter.next()); // { value: "B", done: false }
console.log(iter.next()); // { value: "C", done: false }
console.log(iter.next()); // { value: undefined, done: true }
// === for...of ทำงานเหมือนกัน ===
for (const item of arr) {
console.log(item); // "A", "B", "C"
}
// === เรียก next() ต่อหลังจาก done: true ===
const iter2 = arr[Symbol.iterator]();
iter2.next(); // { value: "A", done: false }
iter2.next(); // { value: "B", done: false }
iter2.next(); // { value: "C", done: false }
// หลังจาก done: true — เรียก next() อีกก็ยังคืน done: true
console.log(iter2.next()); // { value: undefined, done: true }
console.log(iter2.next()); // { value: undefined, done: true }| สถานะของ next() | done | value | ความหมาย |
|---|---|---|---|
| ระหว่างวน — ยังมีค่าถัดไป | `false` | ค่าถัดไปในลำดับ | `for...of` รับค่านี้เข้า loop body |
| ถึงจุดสิ้นสุดของลำดับ | `true` | `undefined` (ไม่มีความหมาย) | `for...of` หยุดวนทันที |
| เรียกหลังจาก done: true แล้ว | `true` | `undefined` | iterator ถึงจุดสิ้นสุดแล้ว — ไม่มีการรีเซ็ต |
- iterator คือ object ที่มี `next()` method — ทุกครั้งที่เรียก `next()` จะคืน `{value, done}`
- อย่าลืมคืน `done: true` — ถ้าลืม `for...of` จะวนไม่หยุด (infinite loop)
- iterator ทุกตัวมี `[Symbol.iterator]()` ที่คืน `this` — จึงใช้กับ `for...of` ได้โดยตรง
- หลังจาก `done: true` — เรียก `next()` อีกก็ยังคืน `done: true` — iterator ไม่รีเซ็ตตัวเอง
Built-in iterables — Array, String และโครงสร้างข้อมูลที่ JavaScript เตรียม `Symbol.iterator` มาให้
JavaScript มี built-in types หลายตัวที่ implement iterable protocol มาให้ — คุณใช้มันกับ `for...of` และ spread ได้โดยไม่ต้องเขียนอะไรเพิ่ม **Built-in iterables ที่คุณเจอแล้ว**: - **Array** — `[...[1,2,3]]` / `for (const v of arr)` - **String** — `for (const ch of "hello")` / `[..."abc"]` → `["a","b","c"]` - **`Object.keys()` / `Object.values()` / `Object.entries()`** — คืน array (iterable) **Built-in iterables ที่จะเจอในบทถัดไป**: - **Map** — วนได้ทั้ง key, value, และ [key, value] pair - **Set** — วนค่า unique แต่ละตัว **Built-in iterables ใน DOM และ environment**: - `arguments` — array-like object ภายใน function - `NodeList` — ผลลัพธ์จาก `document.querySelectorAll()` - `TypedArray` — เช่น `Uint8Array`, `Float32Array` การรู้ว่า type ไหนเป็น iterable ช่วยให้คุณเขียนโค้ดที่ reuse ได้ — เขียน function ที่รับ "อะไรก็ได้ที่เป็น iterable" แทนที่จะรับเฉพาะ array
// === Array (รู้แล้ว) ===
console.log([..."hello"]); // ["h", "e", "l", "l", "o"]
// === String (รู้แล้ว) ===
for (const ch of "AB") console.log(ch); // "A", "B"
// === Object.keys/values/entries — คืน array (iterable) ===
const user = { id: 1, name: "Mali" };
console.log(Object.keys(user)); // ["id", "name"]
console.log(Object.values(user)); // [1, "Mali"]
console.log(Object.entries(user)); // [["id", 1], ["name", "Mali"]]
// === Map — default iterator วน [key, value] ===
const m = new Map([["a", 1], ["b", 2]]);
for (const [key, val] of m) {
console.log(key, val); // "a" 1, "b" 2
}
console.log([...m.keys()]); // ["a", "b"]
// === Set — default iterator วน values ===
const s = new Set([1, 2, 2, 3]);
console.log([...s]); // [1, 2, 3]
// === arguments (ใน function ปกติ) — iterable ===
function show() {
console.log([...arguments]); // [1, 2, 3]
}
show(1, 2, 3);| Type | iterable? | ใช้กับอะไรได้บ้าง | ข้อสังเกต |
|---|---|---|---|
| Array | ✅ | `for...of`, `...`, `[a,b]=` | รู้แล้ว — ใช้ทุกวัน |
| String | ✅ | `for...of`, `...`, `[a,b]=` | วนได้ทีละ character |
| Map | ✅ | `for...of`, `...`, `.keys()`, `.values()` | default iterator วน [key, value] |
| Set | ✅ | `for...of`, `...`, `.keys()`, `.values()` | default iterator วน values |
| `Object.keys()` return | ✅ (คืน array) | `for...of` กับ key | plain object ไม่ใช่ iterable — ใช้ .keys() ล้อม |
| `arguments` | ✅ | `for...of`, `...` | ใช้ใน function ปกติ (ไม่ใช่ arrow) |
| `NodeList` | ✅ | `for...of`, `...` | ผลลัพธ์จาก querySelectorAll |
| Plain object `{}` | ❌ | ใช้ `for...of` ไม่ได้ | object โดยตรงไม่ implement Symbol.iterator |
- built-in iterables ทุกตัวใช้กับ `for...of`, spread (`...`), และ array destructuring (`[a, b] = ...`)
- `Object.keys()` / `Object.values()` / `Object.entries()` เป็นสะพานจาก plain object สู่ iterable — คืน array ที่ใช้ `for...of` ได้
- Map และ Set มี iterator หลายแบบ — `.keys()`, `.values()`, `.entries()` ให้เลือกเรียกตามต้องการ
- รู้ว่า type ไหน iterable → เขียน function ทั่วไปที่รับ iterable ได้ — ไม่ต้องตรวจ type ก่อน
สร้าง custom iterable — เพิ่ม `[Symbol.iterator]()` ให้ object ของคุณเอง
การเป็น iterable ไม่ได้จำกัดอยู่แค่ built-in types — คุณสามารถทำให้ object ธรรมดาเป็น iterable ได้ แค่ implement `[Symbol.iterator]()` **Use case ที่เจอบ่อย**: - สร้าง object ที่เก็บ collection ภายใน — อยากให้ `for...of` วนได้ - สร้าง range generator — อยากให้วนตัวเลขตั้งแต่ start ถึง end - สร้าง lazy data source — อยากให้วนข้อมูลที่คำนวณทีละตัว โดยไม่ต้องเก็บทั้งหมดใน array ก่อน **กฎของการสร้าง iterable**: 1. Object ต้องมี method `[Symbol.iterator]()` 2. Method นั้นต้องคืน iterator — object ที่มี `next()` 3. `next()` ต้องคืน `{value: …, done: …}` — ทุกครั้งจนกว่า `done` จะเป็น `true` **สำคัญ**: ทุกครั้งที่เรียก `[Symbol.iterator]()` — ควรคืน iterator ตัวใหม่ (เริ่มวนจากต้นเสมอ) — เพื่อให้ `for...of` ใช้ซ้ำได้
const counter = {
[Symbol.iterator]() {
let current = 1;
const last = 3;
// คืน iterator object ที่มี next()
return {
next() {
if (current <= last) {
return { value: current++, done: false };
}
return { value: undefined, done: true };
},
};
},
};
// ใช้ for...of — ได้ผลเหมือน array
for (const n of counter) {
console.log(n); // 1, 2, 3
}
// ใช้ spread — กระจายเป็น array
console.log([...counter]); // [1, 2, 3]
// เรียก [Symbol.iterator]() อีกครั้ง — เริ่มวนใหม่
for (const n of counter) {
console.log(n); // 1, 2, 3 ← รอบใหม่ เริ่มจาก 1 อีกครั้งตัวอย่างด้านบนคือ counter แบบ hard-coded — ดีสำหรับเข้าใจโครงสร้าง แต่ในงานจริง คุณมักต้องการ iterable ที่เปลี่ยนค่าได้ ตัวอย่างต่อไปสร้างฟังก์ชัน `makeRange(start, end)` ที่คืน iterable — รับ start และ end เป็น parameter — iterator จะนับจาก start ไปจนถึง end การ wrap ไว้ใน function ทำให้คุณ reuse ตรรกะเดิมกับ parameter ใหม่ — เหมือนเรียก `new Array(n)` แต่เป็น iterable ที่ไม่ต้องจอง memory ล่วงหน้า
function makeRange(start, end) {
return {
[Symbol.iterator]() {
let current = start;
return {
next() {
if (current <= end) {
return { value: current++, done: false };
}
return { value: undefined, done: true };
},
};
},
};
}
const range1to4 = makeRange(1, 4);
console.log([...range1to4]); // [1, 2, 3, 4]
const range10to12 = makeRange(10, 12);
console.log([...range10to12]); // [10, 11, 12]
// ใช้กับ for...of — logic เดิม วนรอบเอง
for (const n of makeRange(5, 7)) {
console.log(n); // 5, 6, 7
}- iterable ของคุณเองต้องมี `[Symbol.iterator]()` — ใช้ computed key `Symbol.iterator` (ห้ามพิมพ์เป็น string `"Symbol.iterator"`)
- `[Symbol.iterator]()` ต้องคืน object ที่มี `next()` — `next()` ต้องคืน `{value, done}` ทุกครั้ง
- ทุกครั้งที่เรียก `[Symbol.iterator]()` ควรได้ iterator ใหม่ — เพื่อให้วนซ้ำได้หลายรอบ
- ใช้ pattern `function factory()` — สร้าง iterable ที่เปลี่ยน parameter ได้ เหมือนตัวอย่าง `makeRange`
- iterable ไม่ต้องเก็บค่าทั้งหมดในหน่วยความจำ — iterator คำนวณทีละค่า → ประหยัด memory เมื่อข้อมูลใหญ่
ข้อควรระวัง — เรื่องที่มักพลาดเกี่ยวกับ iterable และ iterator
การสร้าง iterable เองมีจุดที่พลาดได้ง่าย ต่อไปนี้คือข้อผิดพลาดที่พบบ่อยและวิธีแก้
| ข้อผิดพลาด | เพราะอะไร | วิธีที่ถูก |
|---|---|---|
| `[Symbol.iterator]()` คืน array โดยตรง | array เป็น iterable แต่ไม่ใช่ iterator — `for...of` ต้องการ iterator ที่มี `next()` | คืน object ที่มี `next()` method — array ไม่มี `next()` |
| ลืมคืน `done: true` หลังวนครบทุกค่า | `for...of` เช็ก `done` ว่าเมื่อไหร่จะหยุด — ถ้า `done` เป็น `false` เสมอ → infinite loop | คืน `{value: undefined, done: true}` เมื่อไม่มีค่าให้วนต่อ |
| คืน `done: true` พร้อม `value` ในรอบสุดท้าย | `done: true` ทำให้ `for...of` หยุดทันที — `value` ในรอบนั้นจะถูกทิ้ง | แยกชัด: รอบที่มีค่า → `{value: ข้อมูล, done: false}` / รอบที่ไม่มีค่าแล้ว → `{value: undefined, done: true}` |
| สร้าง iterator ครั้งเดียว — `[Symbol.iterator]()` คืนตัวเดิมตลอด | iterator เก็บ state — เมื่อวนถึงจุดสิ้นสุดแล้ว ถ้าใช้ iterator ตัวเดิมจะเริ่มจาก done: true ทันที | `[Symbol.iterator]()` ควรคืน iterator ใหม่ทุกครั้ง |
| ลืมว่า object ไม่ใช่ iterable — ใช้ `for...of` กับ `{}` | plain object ไม่มี `[Symbol.iterator]` — `for...of` ใช้กับ object โดยตรงไม่ได้ | ใช้ `for (const key of Object.keys(obj))` — หรือใช้ `for...in` |
- เช็กเสมอว่า `next()` คืน object รูป `{value: ..., done: ...}` — `done` เป็น `boolean`, `value` มีหรือไม่มีก็ได้เมื่อ `done` เป็น `true`
- ทดสอบ iterable ด้วย `console.log([...myIterable])` — ถ้า spread ได้ → iterable ถูกต้อง
- iterator ต้องมี `next()` — iterable ต้องมี `[Symbol.iterator]()` — คนละอย่างกัน
- เมื่อวนครบแล้ว — `done: true` — iterator ควรหยุดที่สถานะ `done: true` ไปตลอด ไม่ต้องรีเซ็ตตัวเอง (แค่สร้าง iterator ใหม่เมื่อ `[Symbol.iterator]()` ถูกเรียกอีกครั้ง)
- plain object ต้องแปลงเป็น array ก่อนใช้ `for...of` — `Object.keys()`, `Object.values()`, `Object.entries()` คือทางออก