Câu 1Với const ids = new Set(["u-1", "u-2"]);, chạy ids.add("u-1");. ids.size là gì?
Map và Set — Bỏ Trùng Lặp và Đếm Theo Key
Học Set giữ mỗi giá trị một lần và Map giữ cặp key-value theo thứ tự: bỏ trùng, thêm xóa, tra theo key.
Gom các tag gắn trên một sản phẩm vào một mảng, cùng một tag cứ lặp đi lặp lại — muốn biết thực sự có bao nhiêu loại khác nhau thì phải kiểm tra bằng includes mỗi lần. Muốn giữ một cặp key-value, như số lượng tồn kho theo mã sản phẩm, cũng gặp đúng vấn đề đó nếu chỉ dùng mảng: bạn sẽ cần hai mảng riêng cho ID và số lượng, khớp nhau theo vị trí, và key của object chỉ giới hạn ở chuỗi và symbol, nên không thể dùng số làm key nguyên vẹn.
Bài này nói về Set, không bao giờ giữ trùng lặp, và Map, có thể dùng cả những thứ khác ngoài chuỗi làm key.
Bỏ Trùng Lặp — Xây Một Set
Giả sử bạn muốn biết có bao nhiêu tag khác nhau thực sự đang được dùng trong một danh sách sản phẩm có gắn tag. Trong một mảng thường, cùng một tag xuất hiện nhiều lần, nên đếm nguyên trạng sẽ làm con số bị thổi phồng. Bạn cần một cấu trúc dữ liệu giữ sẵn các giá trị đã bỏ trùng lặp.
Một Set (cấu trúc dữ liệu giữ mỗi giá trị chỉ một lần) được xây bằng cách truyền vào một mảng, như new Set(tags). new ở đầu là cú pháp bạn đặt trước khi tạo một instance mới của một loại cấu trúc dữ liệu cố định, như Set hay Map.
Vì cùng một giá trị không bao giờ thêm trùng lặp, chỉ cần đọc size là biết số lượng giá trị khác nhau. Hai giá trị có tính là giống nhau hay không theo cùng quy tắc như ===.
Một Set không phải mảng, nên index như uniqueTags[0] sẽ không đọc được gì. Khi cần nó dưới dạng mảng, hãy chuyển đổi bằng spread syntax, như [...uniqueTags].
Thứ tự giữ đúng như lúc các mục lần đầu được thêm vào, nên thứ tự xuất hiện lần đầu của mảng gốc được giữ nguyên không đổi.
const tags = ["Sale", "New", "Sale", "Free Shipping"];
// Truyền một mảng vào, và bạn nhận một Set đã bỏ trùng lặp
const uniqueTags = new Set(tags);
console.log(uniqueTags.size); // 3
// Chuyển lại thành mảng bằng spread syntax (giữ nguyên thứ tự thêm vào)
console.log([...uniqueTags].join(", ")); // Sale, New, Free Shipping
// Mảng gốc không đụng tới
console.log(tags.length); // 4
// Bạn cũng có thể bắt đầu từ một Set rỗng
const empty = new Set();
console.log(empty.size); // 0
Thêm và Xóa Giá Trị — add, has, và delete
Đôi khi bạn muốn ghi lại ID người truy cập và đếm cùng một người là 1 dù họ quay lại bao nhiêu lần. Giữ điều này trong một mảng, và mỗi lần thêm nghĩa là kiểm tra bằng includes trước push — càng nhiều bản ghi chồng lên, việc kiểm tra càng mất nhiều thời gian.
Một Set có add để thêm một giá trị, has để kiểm tra nó có tồn tại không, delete để xóa một cái, và size cho số lượng. add không làm gì khi cùng giá trị đó đi vào, nên bạn không bao giờ cần kiểm tra trước khi thêm.
has trả về true hoặc false, và delete trả về nó có thực sự xóa được gì không.
add cùng một giá trị bao nhiêu lần cũng không bao giờ làm tăng số lượng. Chỉ xóa mới làm giảm nó.const visitors = new Set(["u-102", "u-337"]);
// Kiểm tra nó có tồn tại không
console.log(visitors.has("u-102")); // true
console.log(visitors.has("u-999")); // false
// Thêm một cái. Cùng giá trị không đổi số lượng
visitors.add("u-541");
console.log(visitors.size); // 3
visitors.add("u-541");
console.log(visitors.size); // 3
// Xóa một cái. Nó có thực sự xóa được gì không sẽ trả về
console.log(visitors.delete("u-102")); // true
console.log(visitors.delete("u-102")); // false (đã mất rồi)
console.log(visitors.size); // 2
Giữ Cặp Key-Value — Map
Đôi khi bạn muốn giữ một key và value thành một cặp, như số lượng tồn kho theo mã sản phẩm. Object cũng làm được việc này, nhưng để đếm các key được quyết định lúc chạy — như ID — và điều chỉnh chúng theo thời gian, một cấu trúc dữ liệu có phương thức chuyên dụng sẽ phù hợp hơn.
Một Map (cấu trúc dữ liệu giữ cặp key-value theo thứ tự chúng được đăng ký) được xây bằng new Map(), và bạn đăng ký các mục bằng set(key, value). Để đọc một cái, dùng get(key); để kiểm tra một cái có tồn tại không, has(key); cho số lượng, size.
set cùng key lần nữa, và giá trị bị ghi đè. Để bắt đầu với các mục đã điền sẵn, truyền vào một mảng các cặp, như new Map([["P-001", 12], ["P-002", 5]]).
set đăng ký một cặp, và get tra cứu một giá trị theo key. Một key không có mục nào trả về undefined.const stock = new Map();
// Đăng ký một cặp bằng set
stock.set("P-001", 12);
stock.set("P-002", 5);
console.log(stock.size); // 2
// Tra cứu một giá trị từ key bằng get
console.log(stock.get("P-002")); // 5
console.log(stock.get("P-999")); // undefined
console.log(stock.has("P-999")); // false
// set cùng key lần nữa, và nó ghi đè
stock.set("P-001", 8);
console.log(stock.get("P-001")); // 8
console.log(stock.size); // 2 (số lượng không tăng)
// Bạn cũng có thể xây với các cặp đã điền sẵn
const prices = new Map([["P-001", 3980], ["P-002", 2480]]);
console.log(prices.get("P-001")); // 3980
Không Thể Đọc Giá Trị Của Map Bằng Dot Notation
Viết stock["P-001"] thay vì stock.get("P-001"), và bạn sẽ không nhận lại giá trị đó. Cú pháp đó tìm một property trên chính object Map, không liên quan gì đến các cặp bạn đã đăng ký, nên nó trả về undefined. Định tuyến việc đọc và ghi qua get và set.
Chọn Giữa Map và Object — Kiểu Key và Thứ Tự
Vì nó giữ key và value giống hệt như một object, bạn phải chọn giữa chúng. Khi key cố định tại thời điểm bạn viết code, như các trường cài đặt, một object là đủ; khi bạn đếm các key tăng lên lúc chạy, như ID hay ngày tháng, Map dễ làm việc hơn.
Kiểu key và thứ tự là điều bạn dựa vào để quyết định.
Key của một object bị chuyển thành chuỗi, nên ngay cả viết record[2026], key thực sự là "2026". Một Map dùng bất kỳ giá trị nào bạn truyền làm key đúng như nó vốn có, nên số 2026 và chuỗi "2026" được coi là các key khác nhau.
Thứ tự cũng hoạt động khác — một Map luôn giữ thứ tự các mục được đăng ký.
| Khía Cạnh | Object | Map |
|---|---|---|
| Kiểu được phép làm key | Chuyển thành chuỗi | Bất kỳ kiểu nào, giữ nguyên |
| Thứ tự | Key dạng số nguyên xếp trước | Giữ theo thứ tự đăng ký |
| Đếm số mục | length của Object.keys | size |
| Đọc & ghi | obj.key / obj[key] | get và set |
| Phù hợp nhất cho | Tập hợp trường cố định | Đếm key tăng lên lúc chạy |
Để liệt kê nội dung của một Map, viết [...stock.entries()] để chuyển nó thành mảng các cặp [key, value], mỗi cặp là một mảng 2 phần tử. Điều đó cho phép bạn lấy ra từng cặp một, như entries[0]. Lặp lại cùng một bước cho từng cặp, dù có bao nhiêu cặp, sẽ được nói lại ở bài về vòng lặp.
Khi bạn chỉ cần key, dùng stock.keys() — nhưng vì cái trả về không phải mảng, bạn không thể nối join trực tiếp vào nó. Viết [...stock.keys()] để chuyển nó thành mảng trước.
const scores = new Map([["math", 80]]);
// Một Map giữ key số nguyên vẹn dạng số
scores.set(2026, "tổng kết năm nay");
console.log(scores.get(2026)); // tổng kết năm nay
console.log(scores.get("2026")); // undefined (key khác nhau)
// Key của một object bị chuyển thành chuỗi
const record = {};
record[2026] = "tổng kết năm nay";
console.log(Object.keys(record)[0]); // 2026
console.log(typeof Object.keys(record)[0]); // string
console.log(record["2026"]); // tổng kết năm nay (coi là cùng key)
// Chuyển keys() / entries() thành mảng bằng spread syntax trước khi dùng
console.log([...scores.keys()].join(", ")); // math, 2026
const pairs = [...scores.entries()];
console.log(pairs[0].join(": ")); // math: 80
// Một object xếp key dạng số nguyên trước
const ranking = {};
ranking[3] = "c";
ranking[1] = "a";
console.log(Object.keys(ranking).join(", ")); // 1, 3
Kiểm tra kiến thức
Hãy trả lời từng câu hỏi một.
Câu 2Với stock là một Map, stock.get("P-999") trả về gì cho một key chưa đăng ký?
Câu 3Đăng ký số 2026 làm key. Khác biệt giữa Map và một object là gì?