0

Khai Phá Sức Mạnh Của Map<K, V>: Tại Sao Object Không Thể Thay Thế Được Map Trong Các Dự Án Lớn

Chào anh em Viblo

Khi cần lưu trữ dữ liệu dưới dạng Key - Value (Khóa - Giá Trị) trong JavaScript, có tới 90% anh em sẽ nghĩ ngay đến một Object thuần túy:

const userRoles = {1: 'Admin', 2: 'Dev'};

Hồi mới làm quen với JS, mình cũng coi Object là "chân ái" cho mọi bài toán lưu trữ. Cho đến một ngày, dự án yêu cầu mình phải dùng một đối tượng (Object) hoặc một Hàm (Function) để làm Key, hoặc cần một cấu trúc dữ liệu có thể liên tục thêm/xóa hàng vạn phần tử mà không bóp nghẹt hiệu năng của CPU. Lúc đó, Object thuần bắt đầu bộc lộ những giới hạn chí mạng, và mình buộc phải tìm đến Map.

Nếu anh em mở mã nguồn TypeScript core của đối tượng Map, anh em sẽ thấy interface Map<K, V> được định nghĩa cực kỳ tinh gọn. Hôm nay, từ những trải nghiệm thực chiến, mình sẽ cùng anh em mổ xẻ từng thuộc tính và phương thức của cấu trúc dữ liệu thượng đẳng này nhé!

1. Thuộc tính readonly size: number — Biết Rõ Mình Có Bao Nhiêu "Hàng"

Đối với Object thuần, muốn biết nó có bao nhiêu thuộc tính, bạn phải dùng một đường vòng khá cồng kềnh và tốn tài nguyên: Object.keys(obj).length (với độ phức tạp toán học là O(N)O(N) vì phải duyệt qua toàn bộ bảng để đếm).Với Map, nó cung cấp sẵn thuộc tính size:

  • Cách dùng: map.size
  • Bản chất: Đây là thuộc tính readonly (chỉ đọc). Map tự động cập nhật con số này mỗi khi bạn thêm hoặc bớt phần tử. Do đó, việc lấy size đạt tốc độ tối đa O(1)O(1), gọi phát ăn ngay bất kể Map to cỡ nào.

2. Phương thức set(key: K, value: V): this — Chiêu Thức "Chain" Thần Thánh

Hàm này dùng để thêm mới một phần tử hoặc cập nhật lại giá trị nếu key đã tồn tại.

const myMap = new Map();
myMap.set('name', 'Hoang');
  • Điểm đặc biệt 1 (Bất chấp kiểu dữ liệu): Đối với Object, tất cả các key đều bị ép về kiểu string hoặc symbol. Nhưng với Map, Key có thể là bất cứ thứ gì, từ một số, một mảng, một hàm, cho đến một Object khác.
  • Điểm đặc biệt 2 (Fluent API): Hãy để ý kiểu trả về của hàm là : this (trả về chính instance của Map đó). Điều này cho phép anh em viết code theo kiểu "chaining" cực kỳ clean:
myMap.set(1, 'One').set(2, 'Two').set(3, 'Three');

3. Phương thức get(key: K): V | undefined — Lấy Đúng Người, Đúng Thời Điểm

Truy xuất giá trị dựa vào key. Nếu không tìm thấy, nó sẽ trả về undefined.

Vết sẹo thực chiến về Reference Type (Kiểu tham chiếu): Rất nhiều anh em hí hửng dùng Object làm key như thế này rồi khóc ròng vì tại sao .get() toàn ra undefined:

const map = new Map();
map.set({ id: 1 }, 'Admin'); 
console.log(map.get({ id: 1 })); // 👉 undefined!

Tại sao? Vì hai cái { id: 1 } ở dòng trên và dòng dưới nằm ở hai vùng nhớ hoàn toàn khác nhau trên RAM. Để get được, bạn phải giữ lại tham chiếu của Object đó:

const userKey = { id: 1 };
map.set(userKey, 'Admin');
console.log(map.get(userKey)); // 👉 'Admin' (Ngon ngay!)

4. Phương thức has(key: K): boolean — Vòng Gửi Xe An Toàn

Hàm này trả về true/false cho biết một key có tồn tại trong Map hay không.

  • Tại sao nó an toàn hơn Object? Nếu bạn dùng Object, việc check if (obj[key]) hoặc key in obj có thể bị lỗi nếu key đó trùng với các thuộc tính mặc định của prototype (ví dụ: toString, watch). Map thì hoàn toàn tách biệt, nó không bị dính mã độc Prototype Pollution, giúp hệ thống bảo mật hơn rất nhiều.

5. Phương thức delete(key: K): boolean — Xóa Sạch Dấu Vết

Xóa một phần tử ra khỏi Map dựa vào Key.

  • Cách hoạt động: Khác với toán tử delete obj.key của Object (vốn làm giảm hiệu năng tối ưu hóa của V8 Engine), hàm .delete() của Map được tối ưu tối đa ở tầng cánh gà.
  • Giá trị trả về: Trả về true nếu phần tử đó có tồn tại và đã xóa thành công, trả về false nếu phần tử đó vốn không tồn tại. Rất tiện để viết logic kiểm tra.

6. Phương thức clear(): void — Dọn Dẹp Chiến Trường

Khi bạn muốn reset toàn bộ dữ liệu trong Map về trạng thái trống rỗng size = 0.

  • Thay vì phải chạy vòng lặp để xóa từng key, hoặc gán lại map = new Map() làm mất tham chiếu cũ, hàm .clear() sẽ dọn sạch bách mọi phần tử một cách chớp nhoáng và giữ nguyên định danh của đối tượng Map.

7. Phương thức forEach() — Duyệt Tin Nhắn Theo Đúng Thứ Tự

forEach(callbackfn: (value: V, key: K, map: Map<K, V>) => void, thisArg?: any): void;

Hàm này giúp bạn duyệt qua các phần tử của Map.

  • Bẫy thứ tự tham số: Hãy tỉnh táo! Trong hàm callback của .forEach(), value luôn đứng trước key (y hệt như cách thiết kế của Array.prototype.forEach là phần tử đứng trước index). Nhiều anh em quen tay viết (key, value) => ... là log ra ngược ngạo hết cả đấy.
  • Bảo chứng thứ tự (Insertion Order): Object thuần không cam kết thứ tự các key (nó có thể tự sắp xếp lại nếu key là số). Nhưng Map thì uy tín 100%: Bạn chèn key nào vào trước, khi vòng lặp chạy nó sẽ ra trước.

Bảng So Sánh Sương Sương: Object vs Map

Tiêu chí Object {} Map new Map()
Kiểu của Key Chỉ là String hoặc Symbol Bất kỳ kiểu dữ liệu nào (Obj, Func, Number...)
Thứ tự phần tử Không đảm bảo ổn định Đảm bảo đúng thứ tự chèn vào
Đếm số phần tử Phải tính thủ công (O(N)O(N)) Có sẵn thuộc tính .size (O(1)O(1))
Hiệu năng thêm/xóa Kém khi tần suất cao Cực tốt, tối ưu cho High-frequency

Đúc kết kinh nghiệm

  • Hãy tiếp tục dùng Object khi bạn chỉ cần lưu trữ cấu trúc dữ liệu tĩnh, đơn giản, hoặc cần serialize thành chuỗi JSON để gửi qua API (JSON.stringify không hỗ trợ Map một cách mặc định).
  • Hãy chuyển sang dùng Map ngay và luôn nếu bạn cần làm các bộ đệm (In-memory Cache), quản lý danh sách phần tử thay đổi liên tục, hoặc khi Key của bạn là các thực thể phức tạp.

Anh em trong dự án hiện tại đã chuyển sang dùng Map nhiều chưa, hay vẫn trung thành với Object truyền thống? Có pha "vấp cỏ" nào với Map khiến anh em nhớ đời không? Cùng chia sẻ ở phần bình luận nhé! Happy Coding


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí