Skip to main content
JavaScript 9 mins read Devs2

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.

#JavaScript #Map #Set #WeakMap #WeakSet #Data Structures #ES6 #Collections

Minh họa JavaScript Map và Set với cấu trúc key-value, tập hợp không trùng lặp và phép toán tập hợp

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:

  1. Vì sao cần Map và Set — giới hạn thực sự của Object và Array
  2. Map — toàn bộ API và cách dùng
  3. So sánh Map vs Object một cách có hệ thống
  4. Set — toàn bộ API và cách dùng
  5. Các phép toán tập hợp (union, intersection, difference…) theo chuẩn ES2025
  6. WeakMap / WeakSet — khi nào và vì sao
  7. Ứng dụng thực tế trong code hàng ngày
  8. Hiệu năng và benchmark
  9. 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: forEach của Map nhận tham số theo thứ tự (value, key), ngược với Array.prototype.forEach là (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íMapObject
Kiểu của keyMọi kiểu (object, function, number, boolean, NaN…)Chỉ string hoặc symbol
Thứ tựĐảm bảo thứ tự chènCó thứ tự, nhưng key số bị sắp trước
Kích thướcmap.size — O(1)Object.keys(obj).length — O(n)
Kiểm tra tồn tạimap.has(k) an toàn'k' in obj / Object.hasOwn() — dễ dính prototype
Thêm/xóa nhiềuRất nhanh, tối ưu cho dữ liệu độngXóa chậm hơn (delete gây deoptimize)
Serialize JSONKhông trực tiếpJSON.stringify() được ngay
Prototype mặc địnhKhông có key thừaCó key kế thừa (toString, constructor…)
Cú pháp literalnew Map([...]){ ... } gọn hơn
Destructuring / spreadKhông trực tiếpRất tiện
Phù hợp vớiDữ liệu động, khóa đa dạng, cacheRecord 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ăngMapWeakMap
Key là objectBất kỳChỉ object (không nhận primitive)
Thu hồi bộ nhớ tự độngKhôngCó (weak reference)
sizeCóKhông
Duyệt (forEach, spread)CóKhông
clear()CóKhông
Các phương thứcget/set/has/delete/clearget/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 #private thay 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ên cache.keys().next().value chí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ácArray / ObjectMap / 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ạiO(n)O(1)
Đếm kích thướcO(n) cho ObjectO(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:

  • Map cho key-value với khóa đa dạng, thứ tự chèn và hiệu năng O(1).
  • Set cho 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 / WeakSet cho 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?”.

Recently Used Tools