Map vs Object: When to Use Each in JavaScript
Understand the real differences between Map and Object in JavaScript, when each one shines, and the pitfalls every dev needs to know.
Have you ever needed to store key-value pairs in JavaScript and just thrown everything into an object literal? Same here. Most of the time it works. But there are situations where a Map is a much better choice, and ignoring that costs you performance and creates subtle bugs.
Let's understand when each one actually makes sense.
Mental Model: Stop Confusing the Two
Before any syntax, fix this in your head:
Object = a
structwith named fields. Shape is part of its identity. The engine optimizes it assuming the keys rarely change.Map = an actual hash table. A data structure designed from day one to hold entries that come and go.
Using an Object as a dictionary is like using a filing cabinet as a mailbox. It kinda works until the mail starts piling up.
What Is a Map, Anyway?
Map is a native JavaScript data structure (since ES6) that stores key-value pairs. So far it sounds identical to an object. The crucial difference: any type can be a key. Objects, functions, numbers, even NaN.
Try that with an object. obj[{id: 1}] becomes obj['[object Object]']. Not the same thing.
Iteration: Where Map Shines
Map is natively iterable. No need for Object.keys(), Object.values(), or any gymnastics:
And insertion order is guaranteed. In objects, order exists in practice for string keys, but the spec mixes integer-like keys first, then strings, then symbols. Not always what you'd expect.
WeakMap: The Map That the Garbage Collector Loves
If you need to associate data with objects without preventing them from being garbage collected, WeakMap is the answer:
WeakMap is not iterable and has no .size. That's by design: you shouldn't depend on its contents, only on direct access by reference.
Think of WeakMap as a Post-it stuck to an object. Throw the object in the trash, the Post-it goes with it. No leak, no ceremony.
Map vs Object: The Truth Table
Here's the comparison that matters:
- Keys: Map accepts any type. Object only accepts String/Symbol.
- Order: Map guarantees insertion order. Object does not, fully.
- Size: Map has
.size. Object needsObject.keys().length. - Iteration: Map has native
for...of. Object needs helpers. - Performance with many keys: Map is consistently better.
- JSON: Object serializes natively. Map does not.
Performance: What Actually Happens Under the Hood
V8 optimizes Objects assuming a stable shape (hidden classes). Keep adding and deleting keys and the engine eventually gives up, downgrading the object to a slower dictionary mode. You don't see it in the code, you feel it in the profiler.
Map is built for this workload from scratch:
- Heavy insert/delete of thousands of keys: Map stays roughly linear. Object degrades, sometimes ending up orders of magnitude slower on the same benchmark.
- Lookup on small fixed sets (under ~100 static keys): Object is usually faster, since hidden classes turn property access into something close to a struct field read.
Rule of thumb: static shape, small, read-heavy -> Object. Dynamic shape, big, write-heavy -> Map.
Decision Tree: Which One Do I Actually Pick?
Answer these five questions in order, stop at the first "yes":
- Do my keys need to be objects, functions, or anything non-string? ->
Map(orWeakMapif the key should be GC'd). - Do I want the entry to disappear when the key object is garbage collected? ->
WeakMap. - Am I going to insert/delete keys frequently, or hold thousands of entries? ->
Map. - Is the shape fixed, known at write time, and will this go over the wire as JSON? ->
Object. - Still not sure? ->
Object. Refactor toMapthe day you feel the friction.
War Story: The __proto__ That Ate Production
A team I mentored built a permissions cache as a plain object: cache[userId] = perms. Lookups did if (cache[userId]?.admin). Worked in dev. Worked in staging. Then a pentester registered a user whose ID landed in the cache as __proto__, set a payload that included admin: true, and from that moment every user in the system read admin permissions through the prototype chain.
That's prototype pollution, and it's not a hypothetical. Plain objects inherit from Object.prototype, so assigning to keys like __proto__ doesn't just store data, it can rewire the prototype every other object in the runtime sees.
Swapping the cache for a Map killed the bug in one line. Map keys are opaque strings (or anything else) with no link to the prototype chain. map.set('__proto__', evil) stores an entry called __proto__ and nothing else. No inheritance, no surprise.
If you ever hold user-controlled keys in a dictionary, use Map. Or at minimum Object.create(null). Not next sprint. Now.
The Pitfalls Nobody Tells You About
- Objects as keys are compared by reference, not by value. Two
{id: 1}objects are different keys. JSON.stringify(map)returns'{}'. UseObject.fromEntries(map)or[...map]to serialize.- Map maintains insertion order, but if you need to sort by another criterion, you'll have to recreate the Map.
FAQ
1. Is Map polyfillable for old browsers? Partially. core-js ships a Map polyfill that covers the API, but it can't fake true object-key semantics without leaking memory, and performance is nowhere near native. If you still support IE11, test carefully. Everywhere else, just use it.
2. What about the Records & Tuples proposal?
It's been stuck at Stage 2 at TC39 for years and saw no real movement through 2025. Records would give you deeply immutable, value-compared object-like structures (#{a: 1}), finally fixing "two objects with the same content are different keys." Promising, but don't architect around it.
3. Can I put a Map in React state?
You can, but setState(map) won't trigger a re-render if you mutate in place, because the reference didn't change. Always do setMap(prev => new Map(prev).set(k, v)). Honestly, for most component state, a plain object plus the spread operator is less friction.
4. Does Map play well with Redux?
Not really. Redux assumes plain, serializable state. Redux Toolkit uses Immer, which ignores Maps and Sets unless you opt in via enableMapSet() from immer (and even then, time-travel debugging and JSON.stringify get awkward). For Redux state, stick with objects. For local, non-serialized caches, Map is fine.
5. How do I convert between Object and Map without losing order?
Both preserve insertion order for string keys. Watch out: non-string Map keys get coerced to strings on the way back, so 1 and '1' collide.
Key Takeaways
Use Object when your keys are simple strings, the shape is stable, and you need easy JSON serialization (configs, API responses, and so on). Use Map when you need keys of any type, lots of dynamic insertions and deletions, user-controlled keys, or when the number of entries is large and performance matters.
When in doubt, start with an object. Migrate to Map when you feel friction, or the day a pentester teaches you what __proto__ does. Now you know the option exists, and when it makes a difference.