JavaScript Map và Set: Hướng Dẫn Toàn Diện Từ Cơ Bản Đến Nâng Cao
Làm chủ Map, Set, WeakMap và WeakSet trong JavaScript: khởi tạo, thêm/xóa/truy cập, duyệt, các phép toán tập hợp, so sánh với Object/Array, ứng dụng thực tế, hiệu năng và những cạm bẫy cần tránh.

Giới thiệu
Trong nhiều năm, lập trình viên JavaScript chỉ có hai cấu trúc dữ liệu chính để lưu tập hợp: Object (cho dữ liệu dạng key-value) và Array (cho danh sách có thứ tự). Nhưng cả hai đều có những giới hạn khó chịu. Object chỉ dùng được key là string hoặc symbol, không có thuộc tính size, và dễ dính lỗi prototype pollution. Array thì tra cứu phần tử theo giá trị là O(n) — càng nhiều phần tử càng chậm.
Từ ES6 (ES2015), JavaScript bổ sung hai collection mới giải quyết đúng những vấn đề đó:
Map— cấu trúc key-value với khóa thuộc mọi kiểu dữ liệu, giữ thứ tự chèn, cósize, tra cứu O(1).Set— tập hợp các giá trị không trùng lặp, cósize, kiểm tra tồn tại O(1).
Và sau đó là phiên bản “nhẹ” của chúng: WeakMap và WeakSet — cho phép gắn dữ liệu vào object mà không gây memory leak.
Trong bài viết này, chúng ta sẽ đi từ cơ bản đến nâng cao:
- Vì sao cần Map và Set — giới hạn thực sự của Object và Array
- Map — toàn bộ API và cách dùng
- So sánh Map vs Object một cách có hệ thống
- Set — toàn bộ API và cách dùng
- Các phép toán tập hợp (union, intersection, difference…) theo chuẩn ES2025
- WeakMap / WeakSet — khi nào và vì sao
- Ứng dụng thực tế trong code hàng ngày
- Hiệu năng và benchmark
- Cạm bẫy thường gặp
1. Vì Sao Cần Map và Set?
Trước khi có ES6, đây là cách lập trình viên “giả lập” Map bằng Object:
const cache = {};
// Lưu giá trị
cache['user:42'] = { name: 'An' };
// Kiểm tra tồn tại — có một cái bẫy!
if (cache['toString']) {
console.log('Có tồn tại'); // In ra! Vì Object có sẵn toString trên prototype
}
Vấn đề nằm ở chỗ object kế thừa các thuộc tính từ Object.prototype. cache['toString'] trả về một function kế thừa dù bạn chưa từng set nó. Đây là nguồn gốc của vô số bug.
Vấn đề thứ hai: khóa chỉ được là string. Muốn dùng object làm khóa? Bất khả thi.
const map = {};
const keyA = { id: 1 };
const keyB = { id: 1 };
map[keyA] = 'A';
console.log(map[keyB]); // undefined — keyA bị ép thành string "[object Object]"
console.log(Object.keys(map).length); // 1, nhưng bạn tưởng đã có 2 key
Với Array, vấn đề là hiệu năng tra cứu:
const largeArray = [...Array(100000).keys()];
// Tìm kiếm O(n) — càng lớn càng chậm
largeArray.includes(99999); // phải duyệt gần hết mảng
largeArray.indexOf(99999); // tương tự
// Với Set — O(1), gần như tức thời bất kể kích thước
const largeSet = new Set(largeArray);
largeSet.has(99999); // O(1)
Quy tắc lựa chọn nhanh: cần key-value với khóa đa dạng → Map. Cần kiểm tra tồn tại / khử trùng lặp nhanh → Set. Cần dữ liệu có thứ tự index và biến đổi → Array. Cần record có hình dạng cố định và serialize → Object.
2. Map — Cấu Trúc Key-Value Hiện Đại
2.1. Khởi tạo Map
// Map rỗng
const map1 = new Map();
// Từ mảng các cặp [key, value]
const map2 = new Map([
['name', 'An'],
['age', 30],
['city', 'Hà Nội'],
]);
// Từ một Map khác (tạo bản sao)
const map3 = new Map(map2);
console.log(map3.size); // 3
2.2. Thêm, truy cập, xóa
const user = new Map();
// Thêm / cập nhật
user.set('name', 'An');
user.set('age', 30);
user.set('age', 31); // ghi đè — Map không có 2 key trùng
// Truy cập
console.log(user.get('name')); // 'An'
console.log(user.get('age')); // 31
// Key không tồn tại → undefined (KHÔNG ném lỗi)
console.log(user.get('email')); // undefined
// Kiểm tra tồn tại
console.log(user.has('name')); // true
console.log(user.has('email')); // false
// Xóa một entry
user.delete('age');
console.log(user.size); // 1
// Xóa toàn bộ
user.clear();
console.log(user.size); // 0
Điểm mạnh quan trọng nhất: khóa có thể là bất kỳ kiểu dữ liệu nào.
const metadata = new Map();
const buttonElement = document.querySelector('#submit');
const configObject = { theme: 'dark' };
// Object làm khóa
metadata.set(configObject, { clicks: 0 });
// DOM element làm khóa
metadata.set(buttonElement, { label: 'Gửi' });
// Number, boolean, function — đều được
metadata.set(1, 'một');
metadata.set(true, 'đúng');
metadata.set(() => {}, 'hàm');
console.log(metadata.get(configObject)); // { clicks: 0 }
console.log(metadata.size); // 5
2.3. Duyệt Map
Map là iterable và duy trì thứ tự chèn. Bốn cách duyệt:
const scores = new Map([
['An', 95],
['Bình', 87],
['Cường', 92],
]);
// 1. for...of trên entries() — mặc định khi duyệt Map
for (const [name, score] of scores) {
console.log(`${name}: ${score}`);
}
// An: 95
// Bình: 87
// Cường: 92
// 2. keys()
for (const name of scores.keys()) {
console.log(name);
}
// 3. values()
for (const score of scores.values()) {
console.log(score);
}
// 4. forEach(callback(value, key, map)) — chú ý thứ tự tham số!
scores.forEach((score, name) => {
console.log(`${name} → ${score}`);
});
⚠️ Cạm bẫy:
forEachcủa Map nhận tham số theo thứ tự(value, key), ngược vớiArray.prototype.forEachlà(value, index). Rất nhiều người nhầm chỗ này.
2.4. Chuyển đổi qua lại
const map = new Map([['a', 1], ['b', 2]]);
// Map → Array
[...map]; // [['a', 1], ['b', 2]]
[...map.entries()]; // giống trên
[...map.keys()]; // ['a', 'b']
[...map.values()]; // [1, 2]
// Map → Object (chỉ khi khóa là string/symbol)
Object.fromEntries(map); // { a: 1, b: 2 }
// Object → Map
const obj = { x: 1, y: 2 };
new Map(Object.entries(obj)); // Map { 'x' => 1, 'y' => 2 }
3. Map vs Object — So Sánh Có Hệ Thống
| Tiêu chí | Map | Object |
|---|---|---|
| Kiểu của key | Mọi kiểu (object, function, number, boolean, NaN…) | Chỉ string hoặc symbol |
| Thứ tự | Đảm bảo thứ tự chèn | Có thứ tự, nhưng key số bị sắp trước |
| Kích thước | map.size — O(1) | Object.keys(obj).length — O(n) |
| Kiểm tra tồn tại | map.has(k) an toàn | 'k' in obj / Object.hasOwn() — dễ dính prototype |
| Thêm/xóa nhiều | Rất nhanh, tối ưu cho dữ liệu động | Xóa chậm hơn (delete gây deoptimize) |
| Serialize JSON | Không trực tiếp | JSON.stringify() được ngay |
| Prototype mặc định | Không có key thừa | Có key kế thừa (toString, constructor…) |
| Cú pháp literal | new Map([...]) | { ... } gọn hơn |
| Destructuring / spread | Không trực tiếp | Rất tiện |
| Phù hợp với | Dữ liệu động, khóa đa dạng, cache | Record cố định, config, JSON API |
Ví dụ quyết định thực tế:
// ❌ Object không phù hợp: khóa là object
const listenersByElement = {};
listenersByElement[domNode] = handler; // domNode bị ép thành "[object HTMLDivElement]"!
// ✅ Map xử lý đúng
const listenersByElement = new Map();
listenersByElement.set(domNode, handler);
listenersByElement.get(domNode); // handler
// ✅ Object phù hợp: record có hình dạng cố định, cần serialize
const apiResponse = {
id: 1,
name: 'An',
email: 'an@example.com',
};
JSON.stringify(apiResponse); // chạy ngon
4. Set — Tập Hợp Không Trùng Lặp
4.1. Khởi tạo và thao tác cơ bản
// Rỗng
const set1 = new Set();
// Từ iterable (mảng, string, Map...)
const set2 = new Set([1, 2, 2, 3, 3, 3]);
console.log(set2); // Set(3) { 1, 2, 3 } — tự động khử trùng lặp
// Từ string — lưu ý tách từng ký tự
const chars = new Set('hello');
console.log(chars); // Set(4) { 'h', 'e', 'l', 'o' }
API cơ bản:
const fruits = new Set();
fruits.add('táo');
fruits.add('cam');
fruits.add('táo'); // bỏ qua — đã tồn tại
console.log(fruits.size); // 2
console.log(fruits.has('táo')); // true
console.log(fruits.has('xoài'));// false
fruits.delete('cam');
console.log(fruits.size); // 1
fruits.clear();
console.log(fruits.size); // 0
Lưu ý:
Set.prototype.add()trả về chính Set đó, nên có thể chain được:new Set().add(1).add(2).add(3). Điều này hữu ích khi xây dựng.
4.2. Duyệt Set
const colors = new Set(['đỏ', 'xanh', 'vàng']);
// for...of
for (const color of colors) {
console.log(color);
}
// forEach(callback(value, value, set)) — cả 2 tham số đầu đều là value
colors.forEach((value, valueAgain) => {
console.log(value); // 'đỏ', 'xanh', 'vàng'
});
// Chuyển thành mảng
const arr = [...colors]; // ['đỏ', 'xanh', 'vàng']
const arr2 = Array.from(colors);
Vì forEach của Set nhận (value, value, set) — lấy cảm hứng từ Map cho nhất quán API — nên tham số thứ hai luôn trùng tham số thứ nhất. Đây là điểm gây nhầm lẫn nhưng thực tế hiếm khi ảnh hưởng.
4.3. Ứng dụng kinh điển: khử trùng lặp mảng
// Khử trùng lặp số
const numbers = [1, 2, 2, 3, 4, 4, 4, 5];
const unique = [...new Set(numbers)];
console.log(unique); // [1, 2, 3, 4, 5]
// Khử trùng lặp mảng object — lưu ý phải dựa trên giá trị, không phải tham chiếu
function uniqueBy(arr, keyFn) {
const seen = new Set();
return arr.filter((item) => {
const key = keyFn(item);
if (seen.has(key)) return false;
seen.add(key);
return true;
});
}
const users = [
{ id: 1, name: 'An' },
{ id: 2, name: 'Bình' },
{ id: 1, name: 'An (trùng)' },
];
console.log(uniqueBy(users, (u) => u.id));
// [ { id: 1, name: 'An' }, { id: 2, name: 'Bình' } ]
5. Các Phép Toán Tập Hợp (ES2025)
Từ ES2025, Set được trang bị sẵn các phương thức toán học tập hợp mà trước đây phải tự viết:
const a = new Set([1, 2, 3, 4]);
const b = new Set([3, 4, 5, 6]);
// Hợp (union) — mọi phần tử có trong a hoặc b
a.union(b); // Set { 1, 2, 3, 4, 5, 6 }
// Giao (intersection) — phần tử có trong cả a và b
a.intersection(b); // Set { 3, 4 }
// Hiệu (difference) — phần tử có trong a nhưng không có trong b
a.difference(b); // Set { 1, 2 }
// Hiệu đối xứng (symmetric difference) — có trong a hoặc b nhưng không cả hai
a.symmetricDifference(b); // Set { 1, 2, 5, 6 }
// Quan hệ tập con / tập cha
const c = new Set([1, 2]);
c.isSubsetOf(a); // true — c ⊆ a
a.isSupersetOf(c); // true — a ⊇ c
a.isDisjointFrom(new Set([9, 10])); // true — không có phần tử chung
Trước ES2025, bạn phải tự viết:
// Polyfill cho môi trường cũ
function union(a, b) { return new Set([...a, ...b]); }
function intersection(a, b) { return new Set([...a].filter((x) => b.has(x))); }
function difference(a, b) { return new Set([...a].filter((x) => !b.has(x))); }
function symmetricDifference(a, b) {
return new Set([...difference(a, b), ...difference(b, a)]);
}
💡 Kiểm tra hỗ trợ trình duyệt trước khi dùng trực tiếp trong production:
typeof new Set().union === 'function'. Nếu chưa hỗ trợ, dùng polyfill hoặc hàm tự viết.
6. WeakMap và WeakSet — Bộ Nhớ Không Rò Rỉ
6.1. Vấn đề memory leak với Map
// ❌ Memory leak: Map giữ tham chiếu MẠNH tới key
const cache = new Map();
function processUser(user) {
if (!cache.has(user)) {
cache.set(user, expensiveComputation(user));
}
return cache.get(user);
}
// Mỗi user object được truyền vào sẽ MÃI nằm trong cache,
// ngay cả khi không còn ai dùng nó → không bao giờ được thu hồi.
6.2. WeakMap giải quyết
// ✅ WeakMap: tham chiếu YẾU — key không còn dùng thì tự bị thu hồi
const cache = new WeakMap();
function processUser(user) {
if (!cache.has(user)) {
cache.set(user, expensiveComputation(user));
}
return cache.get(user);
}
// Khi `user` không còn ai tham chiếu, entry tự động biến mất.
Đặc điểm của WeakMap:
| Tính năng | Map | WeakMap |
|---|---|---|
| Key là object | Bất kỳ | Chỉ object (không nhận primitive) |
| Thu hồi bộ nhớ tự động | Không | Có (weak reference) |
size | Có | Không |
Duyệt (forEach, spread) | Có | Không |
clear() | Có | Không |
| Các phương thức | get/set/has/delete/clear | get/set/has/delete |
6.3. WeakSet
// Đánh dấu object đã xử lý mà không giữ chúng sống
const processed = new WeakSet();
function processElement(element) {
if (processed.has(element)) return; // đã xử lý rồi
processed.add(element);
// ... xử lý ...
}
// DOM node bị remove → tự động biến mất khỏi WeakSet
6.4. Ứng dụng thực tế của WeakMap
1) Cache metadata cho DOM element:
const elementData = new WeakMap();
function attachData(element, data) {
elementData.set(element, data);
}
function getData(element) {
return elementData.get(element);
}
// element bị xóa khỏi DOM và không còn tham chiếu → data tự được giải phóng
2) Private data trong class:
const privates = new WeakMap();
class Counter {
constructor() {
privates.set(this, { count: 0 });
}
increment() {
privates.get(this).count++;
}
get value() {
return privates.get(this).count;
}
}
const c = new Counter();
c.increment();
c.increment();
console.log(c.value); // 2
console.log(c.count); // undefined — thực sự private
Trong code hiện đại, bạn có thể dùng class field
#privatethay vì WeakMap cho tính đóng gói. Nhưng WeakMap vẫn hữu ích khi cần gắn dữ liệu vào object từ bên ngoài mà không sửa class.
7. Ứng Dụng Thực Tế
7.1. Đếm tần suất xuất hiện
const votes = ['An', 'Bình', 'An', 'Cường', 'An', 'Bình'];
const tally = new Map();
for (const name of votes) {
tally.set(name, (tally.get(name) ?? 0) + 1);
}
console.log([...tally]);
// [ ['An', 3], ['Bình', 2], ['Cường', 1] ]
// Tìm người được bình chọn nhiều nhất
const winner = [...tally].reduce((max, cur) => cur[1] > max[1] ? cur : max);
console.log(winner[0]); // 'An'
7.2. Loại bỏ phần tử trùng trong khi giữ thứ tự
function dedupeKeepOrder(arr) {
return [...new Set(arr)];
}
console.log(dedupeKeepOrder([3, 1, 3, 2, 1])); // [3, 1, 2]
7.3. Đếm phần tử chung giữa hai mảng (O(n + m))
function countCommon(a, b) {
const setB = new Set(b);
return a.filter((x) => setB.has(x)).length;
}
console.log(countCommon([1, 2, 3, 4], [3, 4, 5, 6])); // 2
7.4. Cache (memoization) với Map
function memoize(fn) {
const cache = new Map();
return function (...args) {
const key = JSON.stringify(args);
if (!cache.has(key)) {
cache.set(key, fn.apply(this, args));
}
return cache.get(key);
};
}
const slowSquare = memoize((n) => {
// giả lập tính toán nặng
return n * n;
});
slowSquare(1000); // tính lần đầu
slowSquare(1000); // lấy từ cache — tức thời
Với
Map, cache có thể phình vô hạn (không tự thu hồi như WeakMap). Trong production nên giới hạn kích thước bằng chiến lược LRU — khi quámaxSize, xóa key cũ nhất (Map giữ thứ tự chèn nêncache.keys().next().valuechính là key cũ nhất).
class LRUCache {
constructor(maxSize = 100) {
this.maxSize = maxSize;
this.map = new Map();
}
get(key) {
if (!this.map.has(key)) return undefined;
const value = this.map.get(key);
this.map.delete(key); // xóa rồi thêm lại → đẩy lên cuối (mới nhất)
this.map.set(key, value);
return value;
}
set(key, value) {
if (this.map.has(key)) this.map.delete(key);
this.map.set(key, value);
if (this.map.size > this.maxSize) {
this.map.delete(this.map.keys().next().value); // xóa key cũ nhất
}
}
}
7.5. Nhóm dữ liệu theo khóa (groupBy)
function groupBy(arr, keyFn) {
const groups = new Map();
for (const item of arr) {
const key = keyFn(item);
if (!groups.has(key)) groups.set(key, []);
groups.get(key).push(item);
}
return groups;
}
const orders = [
{ id: 1, status: 'pending' },
{ id: 2, status: 'shipped' },
{ id: 3, status: 'pending' },
];
console.log(groupBy(orders, (o) => o.status));
// Map {
// 'pending' => [ { id: 1 }, { id: 3 } ],
// 'shipped' => [ { id: 2 } ]
// }
Từ ES2024, JavaScript có sẵn
Object.groupBy()vàMap.groupBy(). Dùng khi môi trường hỗ trợ.
7.6. Loại bỏ request trùng lặp (in-flight deduplication)
const inflight = new Map();
function fetchOnce(url) {
if (inflight.has(url)) return inflight.get(url); // trả lại promise đang chạy
const promise = fetch(url).finally(() => inflight.delete(url));
inflight.set(url, promise);
return promise;
}
// Gọi 3 lần liên tiếp cùng URL → chỉ 1 request mạng thực sự
fetchOnce('/api/data');
fetchOnce('/api/data');
fetchOnce('/api/data');
7.7. Kiểm tra hoán vị / anagram
function isAnagram(a, b) {
if (a.length !== b.length) return false;
return a.split('').sort().join('') === b.split('').sort().join('');
}
// Hoặc đếm tần suất bằng Map
function isAnagramFreq(a, b) {
if (a.length !== b.length) return false;
const count = new Map();
for (const ch of a) count.set(ch, (count.get(ch) ?? 0) + 1);
for (const ch of b) {
if (!count.has(ch)) return false;
const n = count.get(ch) - 1;
if (n === 0) count.delete(ch);
else count.set(ch, n);
}
return count.size === 0;
}
8. Hiệu Năng — Khi Nào Map/Set Thắng
Nguyên tắc cơ bản:
| Thao tác | Array / Object | Map / Set |
|---|---|---|
| Tra cứu theo giá trị | O(n) | O(1) |
| Thêm phần tử | O(1) (push) | O(1) |
| Xóa theo giá trị | O(n) | O(1) |
| Kiểm tra tồn tại | O(n) | O(1) |
| Đếm kích thước | O(n) cho Object | O(1) với size |
Với mảng lớn, khác biệt rất rõ:
const N = 1_000_000;
// Array: O(n) mỗi lần tra cứu
const arr = Array.from({ length: N }, (_, i) => i);
console.time('array');
for (let i = 0; i < 1000; i++) arr.includes(N - 1);
console.timeEnd('array');
// Set: O(1) mỗi lần tra cứu
const set = new Set(arr);
console.time('set');
for (let i = 0; i < 1000; i++) set.has(N - 1);
console.timeEnd('set');
Kết quả điển hình trên máy thông thường: set nhanh hơn array hàng trăm đến hàng nghìn lần ở bài toán tra cứu lặp lại này.
Nhưng đừng lạm dụng. Chi phí thực tế phụ thuộc:
- Khởi tạo Set/Map có chi phí. Nếu bạn chỉ tra cứu một lần,
array.includes()có thể nhanh hơn vì không cần build Set. - Map/Set dùng bộ nhớ nhiều hơn Array/Object cho cùng lượng dữ liệu (do cấu trúc hash table).
- Object literal được engine tối ưu rất tốt khi hình dạng cố định (hidden classes). Đừng chuyển mọi object sang Map một cách mù quáng.
Quy tắc thực dụng: nếu phép tra cứu lặp lại nhiều lần trên tập dữ liệu ổn định → chuyển sang Set/Map. Nếu chỉ duyệt tuần tự một lần → Array thường ổn.
9. Cạm Bẫy Cần Tránh
9.1. So sánh object theo tham chiếu
const s = new Set();
s.add({ id: 1 });
s.add({ id: 1 });
console.log(s.size); // 2 — KHÔNG phải 1! Hai object khác tham chiếu.
// Muốn so sánh theo giá trị, dùng khóa nguyên thủy hoặc chuỗi hóa
s.add(JSON.stringify({ id: 1 }));
9.2. Map không serialize JSON trực tiếp
const map = new Map([['a', 1]]);
JSON.stringify(map); // "{}" — RỖNG! Map không có enumerable property.
// Chuyển sang object/array trước
JSON.stringify([...map]); // '[["a",1]]'
JSON.stringify(Object.fromEntries(map)); // '{"a":1}'
9.3. Map không có cú pháp literal
// ❌ Không tồn tại
// const m = { get, set };
// ✅ Bắt buộc dùng constructor
const m = new Map([['key', 'value']]);
9.4. Nhầm forEach của Map
const m = new Map([['a', 1]]);
// ❌ Nhầm thứ tự — tưởng key là tham số đầu
m.forEach((key, value) => console.log(key, value)); // in: 1 'a' (SAI ý định)
// ✅ Đúng thứ tự: value trước, key sau
m.forEach((value, key) => console.log(key, value)); // in: 'a' 1
9.5. Duyệt và sửa Map/Set cùng lúc
const set = new Set([1, 2, 3, 4]);
// Xóa an toàn khi duyệt: entry đã thăm sẽ không bị thăm lại
for (const x of set) {
if (x % 2 === 0) set.delete(x);
}
console.log([...set]); // [1, 3]
// Nhưng THÊM phần tử mới trong lúc duyệt có thể khiến vòng lặp vô hạn
for (const x of set) {
if (x < 100) set.add(x + 1); // ⚠️ vòng lặp không bao giờ kết thúc!
}
9.6. WeakMap không duyệt được
const wm = new WeakMap();
wm.set({}, 1);
console.log(wm.size); // undefined — không có size
// [...wm] // TypeError: wm is not iterable
// wm.forEach(...) // TypeError: wm.forEach is not a function
9.7. Khóa primitive bị tự động box
const wm = new WeakMap();
wm.set(1, 'x'); // ❌ TypeError: Invalid value used as weak map key
wm.set('str', 'x'); // ❌ TypeError
wm.set({}, 'x'); // ✅ chỉ object (và function) được chấp nhận
10. Tổng Kết Nhanh (Cheat Sheet)
// ===== MAP =====
const m = new Map();
m.set(k, v); // thêm/cập nhật
m.get(k); // lấy (undefined nếu không có)
m.has(k); // kiểm tra
m.delete(k); // xóa 1 entry
m.clear(); // xóa hết
m.size; // số entry
m.keys(); m.values(); m.entries();
m.forEach((v, k) => {});
[...m]; // → [[k, v], ...]
Object.fromEntries(m); // → object (chỉ khóa string/symbol)
// ===== SET =====
const s = new Set();
s.add(v); // thêm (bỏ qua nếu trùng)
s.has(v); // kiểm tra
s.delete(v); // xóa
s.clear(); // xóa hết
s.size; // số phần tử
s.forEach((v) => {});
[...s]; // → [v, ...]
// ES2025:
s.union(o); s.intersection(o); s.difference(o);
s.symmetricDifference(o); s.isSubsetOf(o); s.isSupersetOf(o); s.isDisjointFrom(o);
// ===== WEAKMAP / WEAKSET =====
const wm = new WeakMap(); // key là object, tự thu hồi, không size, không duyệt
const ws = new WeakSet(); // value là object, tự thu hồi
FAQ
Map khác Object ở điểm nào?
Bốn khác biệt cốt lõi. Thứ nhất, khóa của Map có thể là bất kỳ kiểu dữ liệu nào — number, boolean, object, function, thậm chí NaN — trong khi khóa của Object chỉ là string hoặc symbol. Thứ hai, Map giữ nguyên thứ tự chèn. Thứ ba, Map có thuộc tính size và các phương thức get/set/has/delete/clear rõ ràng. Thứ tư, Map không có prototype chứa key mặc định nên an toàn hơn khi dùng dữ liệu do người khác nhập. Đổi lại, Object có cú pháp literal gọn hơn và serialize JSON trực tiếp được.
Khi nào nên dùng Map thay vì Object?
Dùng Map khi khóa không phải string, khi cần thêm/xóa key thường xuyên và cần biết kích thước tức thời, khi cần đảm bảo thứ tự chèn, hoặc khi làm việc với cặp key-value động. Dùng Object khi dữ liệu là cấu trúc cố định, khi cần chuyển JSON qua mạng, hoặc khi muốn dùng destructuring và spread thuận tiện.
Set có thay thế mảng không?
Không. Set chỉ kiểm tra tồn tại và đảm bảo không trùng lặp; nó không hỗ trợ truy cập theo chỉ số, không có map/filter/reduce mặc định, không sắp xếp. Array giữ thứ tự, truy cập theo index, có toàn bộ phương thức biến đổi. Cách tốt nhất thường là dùng song song — Set để khử trùng lặp và tra cứu O(1), Array cho thứ tự và biến đổi.
WeakMap và WeakSet dùng để làm gì?
Chúng chỉ cho phép key (WeakMap) hoặc value (WeakSet) là object, và giữ chúng bằng weak reference. Nếu object không còn ai tham chiếu, nó bị garbage collector thu hồi và entry tự động biến mất. Chúng không có size, không duyệt được. Dùng cho cache theo object, metadata gắn với DOM node, hoặc đánh dấu mà không gây memory leak.
Map có sắp xếp theo thứ tự không?
Chúng giữ nguyên thứ tự chèn, không tự sắp xếp theo giá trị hay khóa. Muốn sắp xếp, chuyển sang mảng rồi sort(): [...myMap.entries()].sort((a, b) => a[1] - b[1]).
Làm sao chuyển đổi qua lại giữa Map, Set, Object và Array?
Array → Set: new Set(arr). Set → Array: [...set]. Array cặp → Map: new Map([[k, v]]). Map → Array: [...map]. Object → Map: new Map(Object.entries(obj)). Map → Object: Object.fromEntries(map) (chỉ khi khóa là string/symbol).
Vì sao hai object giống nhau lại không tìm thấy nhau trong Map/Set?
Vì Map và Set so sánh theo thuật toán SameValueZero (gần giống ===). Với object, === so sánh địa chỉ tham chiếu, không so sánh nội dung. Hai object { id: 1 } khác nhau là hai tham chiếu khác nhau. Muốn so sánh theo giá trị, dùng khóa string/number đã chuẩn hóa.
Kết luận
Map và Set là hai bổ sung quan trọng nhất của JavaScript hiện đại cho cấu trúc dữ liệu. Chúng không thay thế Object và Array — chúng lấp đầy khoảng trống mà hai cấu trúc cũ để lại:
Mapcho key-value với khóa đa dạng, thứ tự chèn và hiệu năng O(1).Setcho tập hợp không trùng lặp và tra cứu O(1), cộng thêm các phép toán tập hợp từ ES2025.WeakMap/WeakSetcho việc gắn dữ liệu vào object mà không gây memory leak.
Nắm vững bốn cấu trúc này, cùng với Array và Object quen thuộc, bạn có trong tay bộ công cụ đầy đủ để chọn đúng cấu trúc cho từng bài toán — và đó chính là kỹ năng phân biệt một lập trình viên JavaScript thành thạo với người mới học.
Hãy thử áp dụng ngay hôm nay: lần tới khi viết array.includes() trong vòng lặp, tự hỏi “có nên là Set không?”; khi gắn metadata vào DOM element bằng object, tự hỏi “có nên là WeakMap không?”.