Article · programming
JavaScript Map และ Set
JavaScript
เรียนรู้ Map และ Set ใน JavaScript ตั้งแต่การจัดเก็บคู่คีย์-ค่า การเพิ่มและลบข้อมูล การวนลูป การกำจัดข้อมูลซ้ำ การเปรียบเทียบกับ Object และ Array ไปจนถึงการประยุกต์ใช้กับข้อมูลจริง
EP 14: JavaScript Map และ Set
ในบทก่อนหน้า เราเรียนรู้การจัดเก็บข้อมูลด้วย Object ซึ่งเหมาะกับข้อมูลที่มีคุณสมบัติเป็นชื่อและค่า แต่เมื่อจำเป็นต้องจัดเก็บคู่คีย์-ค่าที่เพิ่มหรือลบได้บ่อย หรือต้องการเก็บรายการที่ไม่ซ้ำกัน JavaScript มีโครงสร้างข้อมูลอีกสองชนิดที่ควรรู้จัก ได้แก่ **Map** และ **Set**
เป้าหมายของบทนี้คุณจะสามารถสร้าง Map และ Set เพิ่ม อ่าน แก้ไข ลบ และวนลูปข้อมูลได้ เข้าใจความแตกต่างระหว่าง Map, Set, Object และ Array รวมถึงเลือกใช้โครงสร้างข้อมูลให้เหมาะกับโจทย์จริง
1. Map คืออะไร
**Map** คือโครงสร้างข้อมูลที่เก็บรายการในรูปแบบคู่ `key` และ `value` โดยสามารถใช้ค่าหลากหลายชนิดเป็น key ได้ เช่น ข้อความ ตัวเลข Boolean Object หรือแม้แต่ฟังก์ชัน และยังคงลำดับการเพิ่มรายการไว้เมื่อวนลูป
const scores = new Map();
scores.set('Mali', 90);
scores.set('Narin', 85);
scores.set('Ploy', 95);
console.log(scores.get('Mali'));
console.log(scores.has('Narin'));
console.log(scores.size);
เมธอด `set()` ใช้เพิ่มหรือแก้ไขรายการ `get()` ใช้อ่านค่าจาก key `has()` ใช้ตรวจสอบว่ามี key หรือไม่ และ `size` ใช้นับจำนวนรายการ ต่างจาก Object ตรงที่ Map มี API สำหรับจัดการคู่คีย์-ค่าโดยเฉพาะ
2. เพิ่ม แก้ไข และลบข้อมูลใน Map
Map สามารถใช้ key เดิมซ้ำเพื่อเปลี่ยนค่าได้ หากเรียก `set()` ด้วย key ที่มีอยู่แล้ว ระบบจะปรับ value ของรายการนั้นแทนการสร้าง key ซ้ำ ส่วน `delete()` ใช้ลบรายการตาม key และ `clear()` ใช้ลบรายการทั้งหมด
const inventory = new Map();
inventory.set('pen', 100);
inventory.set('book', 50);
inventory.set('pen', 120);
console.log(inventory.get('pen'));
inventory.delete('book');
console.log(inventory.has('book'));
console.log(inventory.size);
inventory.clear();
console.log(inventory.size);
จำให้แม่นMap ไม่เก็บ key ที่ซ้ำกันหลายรายการ การเรียก `set()` ด้วย key เดิมจะเปลี่ยน value ของรายการเดิม และ `size` จะนับจำนวน key ที่ไม่ซ้ำกัน
3. Map รองรับ Key ได้หลายชนิด
จุดเด่นของ Map คือ key ไม่จำเป็นต้องเป็นข้อความเสมอไป สามารถใช้ตัวเลข Boolean หรือ Object เป็น key ได้ อย่างไรก็ตาม หากใช้ Object เป็น key การจับคู่จะพิจารณาจากตัวตนของ Object ไม่ใช่จากการที่ property ภายในมีค่าเหมือนกัน
const data = new Map();
const firstUser = { id: 1 };
const secondUser = { id: 1 };
data.set(100, 'numeric key');
data.set(true, 'boolean key');
data.set(firstUser, 'first user');
console.log(data.get(100));
console.log(data.get(true));
console.log(data.get(firstUser));
console.log(data.get(secondUser));
แม้ `firstUser` และ `secondUser` จะมี property `id` เท่ากัน แต่เป็น Object คนละตัว จึงไม่ใช่ key เดียวกันใน Map ตัวอย่างนี้ `data.get(secondUser)` จะคืนค่า `undefined` เพราะไม่มีรายการที่ใช้ Object ตัวนั้นเป็น key
4. วนลูปข้อมูลใน Map
Map สามารถวนลูปด้วย `for...of` ได้โดยตรง แต่ละรายการจะเป็น Array ที่ประกอบด้วย `[key, value]` นอกจากนี้ยังใช้ `keys()`, `values()`, `entries()` หรือ `forEach()` ได้ตามรูปแบบการทำงานที่ต้องการ
const prices = new Map([
['coffee', 60],
['tea', 45],
['water', 15]
]);
for (const [name, price] of prices) {
console.log(`${name}: ${price} บาท`);
}
console.log([...prices.keys()]);
console.log([...prices.values()]);
prices.forEach((price, name) => {
console.log(name, price);
});
สังเกตว่าพารามิเตอร์ของ `Map.prototype.forEach()` เรียงเป็น `(value, key)` ซึ่งต่างจากรูปแบบการแยกค่าด้วย `[key, value]` ในลูป `for...of` ควรระวังลำดับนี้เมื่อต้องนำไปใช้ในโปรแกรมจริง
5. Set คืออะไร
**Set** คือโครงสร้างข้อมูลสำหรับเก็บค่าที่ไม่ซ้ำกัน หากเพิ่มค่าที่มีอยู่แล้ว Set จะไม่สร้างรายการซ้ำ โดยเหมาะกับการเก็บรายการแท็ก รหัสที่ไม่ซ้ำ รายชื่อผู้เข้าร่วม หรือค่าที่ต้องการตรวจสอบการมีอยู่
const tags = new Set();
tags.add('javascript');
tags.add('web');
tags.add('javascript');
console.log(tags.size);
console.log(tags.has('web'));
console.log(tags.has('python'));
console.log([...tags]);
ในตัวอย่าง แม้จะเพิ่มคำว่า `javascript` สองครั้ง แต่ Set จะเก็บไว้เพียงครั้งเดียว โดยยังรักษาลำดับการเพิ่มค่าที่ไม่ซ้ำกันไว้เมื่อวนลูป
6. เพิ่ม ตรวจสอบ และลบค่าใน Set
Set มีเมธอดสำคัญ ได้แก่ `add()` สำหรับเพิ่มค่า `has()` สำหรับตรวจสอบ `delete()` สำหรับลบค่าที่ระบุ และ `clear()` สำหรับล้างข้อมูลทั้งหมด ส่วน `size` ใช้นับจำนวนค่าที่ไม่ซ้ำกัน
const attendees = new Set(['Mali', 'Narin']);
attendees.add('Ploy');
attendees.add('Mali');
console.log(attendees.size);
console.log(attendees.has('Ploy'));
attendees.delete('Narin');
console.log([...attendees]);
attendees.clear();
console.log(attendees.size);
Set ใช้ตรวจสอบค่าที่มีอยู่ได้สะดวกเมื่อมีข้อมูลจำนวนมากและต้องตรวจสอบการมีอยู่ซ้ำ ๆ Set มักเป็นตัวเลือกที่เหมาะกว่า Array ในเชิงโครงสร้าง แต่ประสิทธิภาพจริงยังขึ้นกับสภาพแวดล้อมและรูปแบบการใช้งาน
7. กำจัดข้อมูลซ้ำใน Array ด้วย Set
งานที่พบได้บ่อยคือการกำจัดค่าซ้ำจาก Array เช่น รายการหมวดหมู่หรือแท็ก เราสามารถแปลง Array เป็น Set แล้วแปลงกลับเป็น Array ด้วย Spread Syntax ได้อย่างกระชับ
const categories = [
'book',
'electronics',
'book',
'clothing',
'electronics'
];
const uniqueCategories = [...new Set(categories)];
console.log(uniqueCategories);
วิธีนี้เหมาะกับค่าพื้นฐาน เช่น String และ Number แต่หาก Array มี Object หลายตัวที่ property เหมือนกัน Set จะยังถือว่าเป็นคนละค่าเมื่อเป็น Object คนละตัว เพราะ Set ใช้หลักการเปรียบเทียบ identity สำหรับ Object
const users = [
{ id: 1, name: 'Mali' },
{ id: 1, name: 'Mali' },
{ id: 2, name: 'Narin' }
];
const uniqueObjects = [...new Set(users)];
console.log(uniqueObjects.length);
const uniqueById = [
...new Map(users.map(user => [user.id, user])).values()
];
console.log(uniqueById);
ตัวอย่าง `uniqueObjects` ยังคงมี Object ทั้งสามตัว เพราะแต่ละตัวถูกสร้างขึ้นคนละ instance ส่วน `uniqueById` ใช้ Map ที่มี `id` เป็น key จึงเหลือหนึ่งรายการต่อหนึ่ง ID โดยเมื่อ ID ซ้ำ ค่าล่าสุดจะเป็นตัวที่ถูกเก็บไว้
8. เปรียบเทียบ Object, Array, Map และ Set
โครงสร้างข้อมูลแต่ละชนิดมีจุดประสงค์ต่างกัน การเลือกใช้ให้เหมาะสมช่วยลดความซับซ้อนของโค้ดและทำให้ผู้อ่านเข้าใจเจตนาของข้อมูลได้เร็วขึ้น
| ชนิดข้อมูล | ลักษณะเด่น | เหมาะกับงาน |
|---|---|---|
| Object | เก็บ property ในรูปแบบ key และ value | ข้อมูลผู้ใช้ สินค้า และการตั้งค่า |
| Array | เก็บรายการตามลำดับและเข้าถึงด้วย index | รายการสินค้า คะแนน หรือข้อมูลที่ต้องประมวลผลเป็นชุด |
| Map | เก็บคู่ key และ value โดย key เป็นค่าได้หลายชนิด | ตารางค้นหาข้อมูล การจับคู่รหัสกับข้อมูล และแคช |
| Set | เก็บค่าที่ไม่ซ้ำกัน | กำจัดข้อมูลซ้ำ ตรวจสอบสมาชิก และเก็บรายการที่ไม่ซ้ำ |
9. เลือกใช้ Map แทน Object เมื่อใด
Object เหมาะกับข้อมูลที่มีโครงสร้างชัดเจนและใช้ชื่อ property เพื่ออธิบายความหมาย เช่น `user.name` ส่วน Map เหมาะเมื่อข้อมูลมีการเพิ่มและลบ key บ่อย ต้องใช้ key ที่ไม่ใช่ข้อความ หรือจำเป็นต้องวนลูปคู่คีย์-ค่าโดยตรง
const userProfile = {
name: 'Mali',
role: 'editor'
};
const sessionById = new Map();
sessionById.set(1001, { userId: 1, active: true });
sessionById.set(1002, { userId: 2, active: false });
console.log(userProfile.name);
console.log(sessionById.get(1001));
for (const [sessionId, session] of sessionById) {
console.log(sessionId, session.active);
}
อย่าเลือกโครงสร้างข้อมูลจากความคุ้นเคยเพียงอย่างเดียวพิจารณารูปแบบ key ความต้องการเรื่องลำดับ การตรวจสอบข้อมูลซ้ำ และ API ที่ต้องใช้ หากข้อมูลมาจากภายนอก ควรตรวจสอบรูปแบบและชนิดข้อมูลก่อนนำไปใช้งานเสมอ
10. Set กับการหาจุดร่วมของข้อมูล
Set สามารถนำมาประยุกต์กับการตรวจสอบความสัมพันธ์ระหว่างรายการ เช่น การหาค่าที่มีอยู่ในทั้งสองชุด หรือการหาค่าที่อยู่ในชุดแรกแต่ไม่อยู่ในชุดที่สอง แม้ JavaScript จะมีเมธอด Set composition ในสภาพแวดล้อมรุ่นใหม่บางรุ่น แต่การเขียนฟังก์ชันเองก็ช่วยให้เข้าใจหลักการและควบคุมความเข้ากันได้กับสภาพแวดล้อมที่ใช้งานได้
function intersection(setA, setB) {
return new Set([...setA].filter(value => setB.has(value)));
}
function difference(setA, setB) {
return new Set([...setA].filter(value => !setB.has(value)));
}
const first = new Set([1, 2, 3, 4]);
const second = new Set([3, 4, 5, 6]);
console.log([...intersection(first, second)]);
console.log([...difference(first, second)]);
ฟังก์ชัน `intersection()` คืนค่าที่อยู่ในทั้งสอง Set ส่วน `difference()` คืนค่าที่อยู่ใน Set แรกแต่ไม่อยู่ใน Set ที่สอง ในตัวอย่างผลลัพธ์คือ `[3, 4]` และ `[1, 2]` ตามลำดับ
11. สร้างระบบนับความถี่ของข้อมูลด้วย Map
Map เหมาะกับการนับความถี่ เช่น คำใดปรากฏกี่ครั้ง หมวดหมู่ใดมีจำนวนเท่าไร หรือสถานะงานแต่ละชนิดมีจำนวนเท่าใด โดยใช้ค่าที่พบเป็น key และจำนวนครั้งเป็น value
function countFrequency(items) {
const frequency = new Map();
for (const item of items) {
const currentCount = frequency.get(item) ?? 0;
frequency.set(item, currentCount + 1);
}
return frequency;
}
const orders = [
'pending', 'paid', 'pending', 'cancelled',
'paid', 'pending'
];
const result = countFrequency(orders);
for (const [status, count] of result) {
console.log(`${status}: ${count}`);
}
การใช้ `frequency.get(item) ?? 0` หมายถึง หากยังไม่มี key นี้ ให้เริ่มนับจากศูนย์ จากนั้นเพิ่มจำนวนหนึ่งและบันทึกกลับด้วย `set()` วิธีนี้ใช้ได้กับข้อมูลหลายประเภท แต่ควรพิจารณาว่าค่าที่นำมาเป็น key มีรูปแบบการเปรียบเทียบที่ตรงกับความต้องการหรือไม่
12. ข้อควรระวังเมื่อใช้ Map และ Set
- Map และ Set เปรียบเทียบ Object ตามตัวตนของ Object ไม่ใช่เปรียบเทียบ property ภายในทีละค่า
- Map ใช้ `size` สำหรับนับรายการ ไม่ใช่ `length`
- Set ใช้ `has()` เพื่อตรวจสอบสมาชิก ไม่ใช่ `includes()` แบบ Array
- Map และ Set ไม่ได้แปลงเป็น JSON โดยตรงในรูปแบบที่เก็บข้อมูลทั้งหมดตามต้องการ ต้องกำหนดวิธีแปลงข้อมูลอย่างชัดเจน
- Map และ Set รักษาลำดับการวนลูปตามลำดับการเพิ่มสมาชิก แต่การลบแล้วเพิ่ม key หรือค่ากลับเข้ามาใหม่อาจทำให้ตำแหน่งเปลี่ยนไป
- หากใช้ Object เป็น key หรือสมาชิก ต้องเก็บ reference ที่ถูกต้องไว้เพื่อค้นหาหรือลบรายการนั้น
13. แปลง Map และ Set เป็น JSON
`JSON.stringify()` ไม่ได้แปลง Map และ Set ให้เป็นโครงสร้างข้อมูลที่ต้องการโดยอัตโนมัติ วิธีทั่วไปคือแปลง Map เป็น Array ของคู่ `[key, value]` และแปลง Set เป็น Array ก่อน จากนั้นจึงสร้างข้อความ JSON
const prices = new Map([
['pen', 10],
['book', 80]
]);
const uniqueTags = new Set(['study', 'coding', 'study']);
const mapJson = JSON.stringify([...prices]);
const setJson = JSON.stringify([...uniqueTags]);
console.log(mapJson);
console.log(setJson);
const restoredMap = new Map(JSON.parse(mapJson));
const restoredSet = new Set(JSON.parse(setJson));
console.log(restoredMap.get('book'));
console.log([...restoredSet]);
ข้อจำกัดของการแปลงข้อมูลการแปลง Map เป็น Array ของคู่จะใช้ได้ตรงไปตรงมาเมื่อ key และ value เป็นข้อมูลที่ JSON รองรับ หาก key เป็น Object หรือมีค่าอย่าง undefined, Function หรือชนิดข้อมูลพิเศษ ต้องออกแบบรูปแบบ serialization และการกู้คืนข้อมูลให้เหมาะสม เพราะ JSON ไม่รักษาความสามารถทุกอย่างของ JavaScript
14. แบบฝึกหัดท้ายบท
กำหนดให้มีรายชื่อผู้สมัครที่อาจซ้ำกัน จงสร้าง Set เพื่อตัดรายชื่อที่ซ้ำออก แล้วสร้าง Map เพื่อนับจำนวนครั้งที่แต่ละชื่อปรากฏในข้อมูลต้นฉบับ โดยไม่แก้ไข Array ต้นฉบับ
const applicants = [
'Mali', 'Narin', 'Mali', 'Ploy',
'Narin', 'Mali'
];
const uniqueApplicants = [...new Set(applicants)];
const applicantCounts = new Map();
for (const name of applicants) {
applicantCounts.set(
name,
(applicantCounts.get(name) ?? 0) + 1
);
}
console.log(uniqueApplicants);
console.log([...applicantCounts]);
เฉลยและเหตุผล
Set จะเก็บชื่อที่ไม่ซ้ำกัน โดยผลลัพธ์คือ Mali, Narin และ Ploy ส่วน Map ใช้ชื่อเป็น key และจำนวนครั้งเป็น value ผลการนับคือ Mali จำนวน 3 ครั้ง Narin จำนวน 2 ครั้ง และ Ploy จำนวน 1 ครั้ง ทั้งสองโครงสร้างข้อมูลถูกสร้างใหม่ จึงไม่แก้ไข Array `applicants` ต้นฉบับ
15. สรุปบทเรียน
Map เหมาะกับการเก็บคู่คีย์-ค่าและจัดการรายการผ่าน `set()`, `get()`, `has()`, `delete()` และ `size` ส่วน Set เหมาะกับการเก็บสมาชิกที่ไม่ซ้ำผ่าน `add()`, `has()`, `delete()` และ `size` ทั้งสองชนิดวนลูปตามลำดับการเพิ่มสมาชิก และช่วยแก้ปัญหาที่ Array หรือ Object อาจไม่ใช่ตัวเลือกที่เหมาะสมที่สุด
เช็กลิสต์ก่อนจบบทคุณควรอธิบายได้ว่า Map ต่างจาก Object อย่างไร Set ต่างจาก Array อย่างไร เหตุใด Object ที่มี property เหมือนกันจึงอาจเป็นสมาชิก Set คนละตัว และจะใช้ Map นับความถี่หรือใช้ Set กำจัดค่าซ้ำได้อย่างไร
การเลือกโครงสร้างข้อมูลที่เหมาะสม ทำให้โค้ดไม่ได้แค่ทำงานได้ แต่ยังสื่อสารความตั้งใจของผู้เขียนได้ชัดเจน
ตอนถัดไป: **JavaScript Error Handling ด้วย try...catch และการจัดการข้อผิดพลาด** — เรียนรู้การตรวจจับข้อผิดพลาด การโยน Error การใช้ finally และการสร้างโค้ดที่รับมือกับสถานการณ์ไม่คาดคิดได้ดีขึ้น
Reach within 500px of the page end and stay for at least 5 minutes to earn EXP once per day.
⏳ Reading timer is running...