I used to reach for plain objects for everything. Key-value lookups, uniqueness checks, counting occurrences — all just {} and Object.keys and hoping for the best. Then I actually read the MDN docs on Map and Set properly and realized I'd been making my life harder for no reason.
Map and Set aren't new. They've been in every major browser since 2015. But I still see codebases that use objects as dictionaries and arrays for uniqueness checks. Here's why that needs to stop ( and what to use instead ).
Why Map Beats Plain Objects
Plain objects work as dictionaries until they don't. Keys are always strings ( or Symbols ). You can't use DOM elements as keys. You can't easily get the size. You can't rely on insertion order in older engines. And Object.prototype pollution is a real attack vector.
Map solves all of this:
const cache = new Map();
const el = document.getElementById('submit-btn');
// You can use ANY value as a key — objects, DOM nodes, even functions
cache.set(el, { clickedAt: Date.now() });
cache.size; // 1 — no Object.keys().length hack
cache.has(el); // true — no hasOwnProperty dance
cache.get(el); // { clickedAt: 1709647200000 }Keys maintain insertion order. Always. Not "mostly" like objects. And you can iterate directly without converting to an array first.
The API Is Actually Good
Map has a proper iterable API. No converting to entries and back.
const settings = new Map([
['theme', 'dark'],
['lang', 'en'],
['notifications', true]
]);
// Iterate — no Object.entries() needed
for (const [key, value] of settings) {
console.log(key, value);
}
// Destructure into entries
const entries = [...settings]; // [['theme','dark'], ...]
// Merge maps ( yes, spread works )
const merged = new Map([...defaults, ...overrides]);That last one is particularly useful. Default settings that get overridden by user preferences — one line, no mutation.
Set Exists for a Reason
I've lost count of how many times I've seen this pattern:
// DON'T — the array uniqueness shuffle
const unique = [...new Set(array)];
// Or worse:
const seen = {};
const unique = arr.filter(item => {
if (seen[item]) return false;
seen[item] = true;
return true;
});That first one is fine for deduplication ( I'll get to that ). But if you're maintaining a collection of unique values that you add to and remove from, use a Set directly.
const activeUsers = new Set();
activeUsers.add('user_123');
activeUsers.add('user_456');
activeUsers.add('user_123'); // no-op, already exists
activeUsers.size; // 2
activeUsers.has('user_123'); // true — O(1) lookup
activeUsers.delete('user_456'); // true
activeUsers.clear(); // empty the whole thingWhen You Actually Need WeakMap
WeakMap and WeakSet exist for one specific reason: you want to attach data to objects without leaking memory. Keys must be objects. When the object is garbage collected, the entry disappears too.
// Attach metadata to DOM nodes without leaking
const expansions = new WeakMap();
function toggleExpand(el) {
const state = expansions.get(el) || { expanded: false };
state.expanded = !state.expanded;
expansions.set(el, state);
el.classList.toggle('expanded', state.expanded);
}
// When the DOM node is removed, the WeakMap entry vanishes too.
// No cleanup needed.I use WeakMap for caching expensive computations attached to specific objects. When the object goes away, the cache goes with it. No memory leak, no manual invalidation.
Performance Is Not What You Think
People assume Map is slower than {}. For small collections ( under ~100 entries ), plain objects are slightly faster for lookups. For anything larger, Map wins. And for frequent additions and deletions, Map is significantly faster because it doesn't need to handle V8's hidden class transitions.
Set.has() is O(1). Array.includes() is O(n). If you're checking membership in a list more than once, convert to a Set.
// Bad — O(n) every time
const allowedRoles = ['admin', 'editor', 'moderator'];
if (allowedRoles.includes(user.role)) { /* ... */ }
// Better — O(1) lookup, set up once
const ALLOWED_ROLES = new Set(['admin', 'editor', 'moderator']);
if (ALLOWED_ROLES.has(user.role)) { /* ... */ }Real-World Patterns I Use
Here are a few patterns that come up constantly in my code:
// 1. Rate limiting with Map
const rateLimiter = new Map();
function isAllowed(ip) {
const now = Date.now();
const last = rateLimiter.get(ip);
if (last && now - last < 1000) return false;
rateLimiter.set(ip, now);
return true;
}
// 2. Counting with Map
const wordCount = new Map();
for (const word of text.split(/\s+/)) {
wordCount.set(word, (wordCount.get(word) || 0) + 1);
}
// 3. Deduplication + preservation of order
const uniqueIds = [...new Set(response.map(r => r.id))];When NOT to Use Them
Don't use Map when you're just passing around JSON from an API. That's a plain object's job. Don't use Set when you need duplicates — it literally exists to prevent them. And don't use WeakMap if you need to iterate over entries, because you can't.
Also, Map and Set don't serialize to JSON. If you need to send one over the wire, you'll convert to/from a plain object or array anyway. Know your use case.
Conclusion
Map and Set aren't fancy replacements for objects and arrays. They solve specific problems that objects and arrays solve badly — keyed collections with arbitrary keys, and unique value collections with O(1) lookups. Use them for those problems. Use objects and arrays for everything else.
The MDN docs are thorough. Read them once and you won't go back.