Menu

Map và Set trong JavaScript: khi nào dùng thay object và mảng

Map và Set trong JavaScript hoạt động thế nào, khác gì so với object và mảng thông thường, và khi nào dùng chúng thật sự có lợi.

Trang này có trình soạn thảo chạy trực tiếp. Sửa, chạy và xem đầu ra ngay lập tức.

Hai tập hợp ngoài object và mảng

Object và mảng thông thường đáp ứng hầu hết nhu cầu của chương trình JavaScript, nhưng chúng không được thiết kế cho mọi việc. Map và Set là các cấu trúc tập hợp có sẵn, lấp đầy hai khoảng trống cụ thể: tra cứu theo khóa khi khóa không phải chuỗi, và kiểm tra phần tử với giá trị không trùng lặp.

Chúng có mặt trong ngôn ngữ từ ES2015. Cả hai đều là iterable, đều có thuộc tính .size, và đều dùng tốt với toán tử spread. Cách nghĩ rất đơn giản:

  • Map: giống object, nhưng khóa có thể là bất cứ thứ gì và thứ tự được giữ nguyên.
  • Set: giống mảng, nhưng giá trị là duy nhất và tra cứu rất nhanh.

Tạo và dùng Map

Một Map chứa các cặp khóa/giá trị. Bạn tạo nó bằng new Map() và dùng .set(), .get(), .has() và .delete():

Bạn cũng có thể truyền một mảng các cặp [key, value] vào hàm khởi tạo để có sẵn dữ liệu ban đầu:

Dạng mảng hai phần tử đó xuất hiện ở mọi nơi có Map: đó là cách các phần tử được biểu diễn khi bạn duyệt.

Map và object: dùng Map để làm gì?

Object thông thường trông như làm được cùng việc. Phần lớn thời gian thì đúng vậy. Nhưng Map khắc phục vài điểm bất tiện cụ thể:

Object kế thừa từ Object.prototype, nên các khóa như toString, constructor và hasOwnProperty đã có sẵn trên mọi object. Map không mang theo những thứ đó: chỉ những khóa bạn đặt mới tồn tại.

Những khác biệt khác nên biết:

  • Khóa thuộc bất kỳ kiểu nào. Map chấp nhận object, hàm, số, boolean làm khóa. Object lặng lẽ chuyển khóa không phải chuỗi thành chuỗi: obj[1] và obj["1"] là cùng một ô.
  • Đảm bảo thứ tự thêm vào. Map duyệt theo thứ tự các phần tử được thêm vào. Object phần lớn cũng vậy, nhưng các khóa chuỗi trông giống số sẽ được sắp xếp lên trước, một cái bẫy khó thấy.
  • Có sẵn kích thước. map.size là O(1). Với object, bạn phải viết Object.keys(obj).length, tức là tạo lại cả một mảng.
  • Tối ưu cho việc thêm xóa liên tục. Engine tối ưu Map cho việc thêm và xóa thường xuyên. Object được tối ưu cho các bản ghi có cấu trúc ổn định.

Dùng object khi bạn mô hình hóa một bản ghi với các khóa chuỗi đã biết trước ({ name, email, age }). Dùng Map khi khóa thay đổi động, không phải chuỗi, hoặc khi bạn sẽ thêm và xóa phần tử nhiều.

Duyệt Map

Map là iterable, nghĩa là for...of dùng được trực tiếp và việc tách từng cặp ra rất tự nhiên:

Nếu chỉ muốn lấy khóa hoặc chỉ lấy giá trị, hãy gọi .keys() hoặc .values(). Và nếu thích, bạn vẫn có .forEach():

Để chuyển Map trở lại thành object hoặc mảng thông thường, hãy spread nó:

Tạo và dùng Set

Một Set chứa các giá trị duy nhất. Thêm một giá trị đã có sẵn sẽ không làm gì cả:

Tính duy nhất được xác định theo cùng quy tắc so sánh với ===, với một điểm đặc biệt: NaN được coi là bằng chính nó bên trong Set, dù NaN === NaN là false ở mọi nơi khác.

Truyền một iterable vào hàm khởi tạo để có sẵn dữ liệu trong Set, và mẹo loại bỏ phần tử trùng lặp bắt nguồn từ đây:

Một dòng, dùng được với mọi kiểu nguyên thủy. Với mảng chứa object thì cách này không hiệu quả, vì hai object khác nhau có cùng các trường vẫn là hai giá trị khác nhau, nhưng với chuỗi, số và boolean thì đây là cách loại bỏ trùng lặp chuẩn mực.

Set và mảng: khi nào nên chuyển

Mảng và Set đều chứa một tập hợp giá trị, vậy khi nào chọn cái nào?

Hãy dùng Set khi:

  • Giá trị phải là duy nhất và bạn muốn môi trường chạy đảm bảo điều đó.
  • Bạn kiểm tra phần tử rất nhiều lần. set.has(x) là O(1); array.includes(x) là O(n). Bên trong vòng lặp, khác biệt đó tăng lên rất nhanh.
  • Thứ tự thêm vào là tất cả những gì bạn cần. Set duyệt theo thứ tự thêm vào nhưng không hỗ trợ truy cập theo chỉ số.

Hãy giữ mảng khi:

  • Bạn cần truy cập theo vị trí: arr[0], cắt mảng, sắp xếp.
  • Phần tử trùng lặp có ý nghĩa: một giỏ hàng có hai món giống nhau.
  • Bạn sẽ dùng nhiều phương thức mảng như .map, .filter, .reduce. Set không có những phương thức này; bạn phải spread ra mảng trước.

Một ví dụ nhanh liên quan đến hiệu năng:

Nếu banned là mảng, mỗi lần callback của filter chạy sẽ phải quét cả danh sách. Khi là Set, mỗi lần tra cứu mất thời gian hằng số.

Duyệt Set

Giống như Map: for...of dùng được ngay, và spread cho bạn một mảng:

Set cũng có .keys(), .values() và .entries() để đối xứng với Map, dù với Set thì khóa và giá trị là một. Phần lớn thời gian bạn chỉ cần duyệt trực tiếp.

Ví dụ hoàn chỉnh: đếm số khách truy cập duy nhất của mỗi trang

Kết hợp cả hai: một Map từ đường dẫn trang đến một Set chứa ID khách truy cập:

Map lo việc ánh xạ từ đường dẫn đến từng nhóm; Set lo việc loại bỏ trùng lặp trong mỗi nhóm. Làm điều tương tự với object và mảng thông thường vẫn được, nhưng bạn sẽ phải viết thêm các phép kiểm tra indexOf và hasOwnProperty suốt quá trình.

Sơ lược về WeakMap và WeakSet

Có hai cấu trúc liên quan dành cho một trường hợp sử dụng hẹp: WeakMap và WeakSet. Chúng giữ tham chiếu yếu, nghĩa là một phần tử có khóa (với WeakMap) hoặc giá trị (với WeakSet) không còn tham chiếu nào khác sẽ tự động bị bộ thu gom rác thu hồi.

Chúng chỉ nhận object làm khóa, không phải iterable và không có .size. Đó là chủ ý thiết kế: nếu duyệt được chúng, người ta sẽ quan sát được hoạt động của bộ thu gom rác. Chúng hữu ích để lưu cache metadata về các object không thuộc quyền quản lý của bạn, và hiếm gặp trong code hằng ngày.

Tiếp theo: JSON

Map và Set rất tốt khi ở trong bộ nhớ, nhưng cả hai đều không giữ nguyên được qua JSON.stringify: Map thành {} và Set cũng thành {}. Trang tiếp theo nói về JSON: cách tuần tự hóa và parse dữ liệu, cùng các mẫu xử lý những tập hợp được giới thiệu ở trang này khi chúng cần đi qua mạng hoặc vào một tệp.

Câu hỏi thường gặp

Map và Object trong JavaScript khác nhau thế nào?

Một Map có thể dùng bất kỳ giá trị nào làm khóa: object, hàm, số, mọi thứ, trong khi object chuyển khóa thành chuỗi (hoặc symbol). Map cũng theo dõi kích thước qua .size, duyệt theo thứ tự thêm vào và không kế thừa khóa từ prototype, nên không có nguy cơ trùng với toString hay constructor. Hãy dùng Map khi khóa không phải chuỗi hoặc khi bạn cần thêm và xóa phần tử thường xuyên.

Set trong JavaScript dùng để làm gì?

Một Set lưu các giá trị duy nhất: nó lặng lẽ bỏ qua giá trị trùng lặp. Cách nhanh nhất để loại bỏ phần tử trùng trong mảng là [...new Set(arr)]. Set cũng cho bạn phép kiểm tra .has() với độ phức tạp O(1), nhanh hơn array.includes() khi bạn kiểm tra phần tử bên trong một vòng lặp.

Làm sao duyệt qua một Map?

for...of dùng được trực tiếp: for (const [key, value] of myMap) tách từng cặp ra. Bạn cũng có thể lặp qua myMap.keys(), myMap.values() hoặc myMap.entries(). Thứ tự duyệt được đảm bảo khớp với thứ tự thêm vào, điều mà object thường không phải lúc nào cũng đảm bảo với các khóa trông giống số.

Hình minh họa các ngôn ngữ lập trình của Coddy

Học lập trình cùng Coddy

Bắt đầu