Back to JavaScript
JavaScript
easy
junior

When should you reach for Map versus a plain Object in JavaScript?

Object: keys are strings/symbols, key order is mostly insertion (with integer-key quirks), prototype chain pollution. Map: any key type (objects, NaN), guaranteed insertion order, .size in O(1), better for frequent add/remove. Use Map for dictionaries; Object for shaped records.

5 min read·~10 min to think through

Object and Map both store key-value pairs, but they have different design contracts. Junior code uses Object for everything; the senior signal is knowing when Map is the better choice and why.

Key differences.

FeatureObjectMap
Key typesstring, symbolanything (objects, functions, NaN, primitives)
Iteration orderinsertion (integer keys sort numerically first)guaranteed insertion
SizeObject.keys(o).length — O(n)map.size — O(1)
Prototypeinherits from Object.prototype (__proto__, constructor, toString...)none
Iterationrequires Object.keys/entries/valuesdirectly iterable (for…of, forEach)
JSONJSON.stringify(obj) worksnot directly serializable — convert to array of pairs
Performanceoptimized by V8 for "shape-stable" recordsoptimized for frequent insert/delete
Memorysmaller per entry for small objectslarger overhead per entry

When to use Map.

  • Dictionaries with arbitrary keys — especially keys you don't control (user-provided strings could collide with __proto__, constructor).
  • Object keys — caching/memoization keyed by an object reference, WeakMap for the same with GC.
  • Frequent add/remove — Map is implemented for this; Object reshaping triggers V8 deopts.
  • Insertion-order matters always — Object's integer-key reordering surprises people: {2:"a", 1:"b"} iterates 1,2.
  • You need .size cheaply — Map gives it in O(1).

When to use Object.

  • Records with a known shape{ id, name, email }. V8 generates hidden classes; access is faster.
  • JSON I/O — APIs and storage round-trip Object naturally.
  • Function arguments / config bags — destructuring works directly.
  • Shorthand syntax{ a, b } is more concise than new Map([["a", a], ["b", b]]).

The prototype-pollution gotcha.

ts
const cache = {};
cache["__proto__"] = "owned";       // doesn't actually set the key — modifies prototype
cache["hasOwnProperty"] = false;    // breaks future calls if you do cache.hasOwnProperty(k)

Defenses: Object.create(null) (no prototype), Object.hasOwn(obj, key) (modern), or just use Map which has no prototype.

Iteration:

ts
// Map (clean)
for (const [k, v] of map) { ... }

// Object (need to choose what you want)
for (const k of Object.keys(obj)) { ... }
for (const [k, v] of Object.entries(obj)) { ... }

Performance reality. For 100k entries with frequent add/delete, Map is meaningfully faster than Object. For 100 stable-shape records, Object wins on memory and access. Don't optimize without a profile.

Sets vs Map. Set is the value-only equivalent of Map. Use it for "is this in the collection?" — replaces ad-hoc { [key]: true } patterns.

Weak variants. WeakMap / WeakSet hold object keys weakly so they can be garbage-collected. Useful for: tagging DOM elements without leaking when removed, caches keyed by component instance, private-data patterns. Trade-off: not iterable, no .size.

Decision shortcut.

  • Plain record? → Object.
  • Hash table / dictionary with dynamic keys? → Map.
  • Object as key, want GC? → WeakMap.
  • Set membership? → Set.

Code

ts
// Object key cache
const measureCache = new WeakMap<HTMLElement, DOMRect>();
function getRect(el: HTMLElement) {
  let rect = measureCache.get(el);
  if (!rect) { rect = el.getBoundingClientRect(); measureCache.set(el, rect); }
  return rect;
}

// Frequency counter — guaranteed order, .size, no prototype
const counts = new Map<string, number>();
for (const word of words) counts.set(word, (counts.get(word) ?? 0) + 1);
const top = [...counts.entries()].sort((a,b) => b[1] - a[1]).slice(0, 10);
When Map shines
ts
const o = {};
o["2"] = "a";
o["1"] = "b";
o["foo"] = "c";
Object.keys(o); // ["1", "2", "foo"] — integer keys reordered ascending first
// Map preserves true insertion order: [["2","a"], ["1","b"], ["foo","c"]]
Object integer-key reorder gotcha

Follow-up questions

  • Why does Object reorder integer keys?
  • What's the use case for WeakMap?
  • How would you make a 'safe' Object dictionary?
  • When would you serialize a Map to JSON?

Common mistakes

  • Using Object for user-provided keys → prototype pollution.
  • Reaching for Object.keys(obj).length in a hot loop instead of tracking size.
  • Trying to use a Date or Object as an Object key — coerces to '[object Object]'.
  • Forgetting Map iteration is direct — wrapping in Object.entries unnecessarily.

Performance considerations

  • V8 optimizes stable-shape Objects (hidden classes); reshaping triggers deopts.
  • Map outperforms Object for dynamic-key workloads at scale.
  • WeakMap avoids leaks when keying by long-lived objects.

Edge cases

  • NaN as a Map key works once (NaN === NaN inside Map); Object coerces NaN to 'NaN' string.
  • Map preserves insertion order including numeric keys — Object does not.
  • JSON.stringify(map) → '{}' — convert to entries first.

Real-world examples

  • React internals use Map/WeakMap for keyed children and component state caches; React Router uses Map for route params.

Senior engineer discussion

Senior signal: knowing when Map's contract pays off (object keys, dynamic adds, prototype safety) and when Object's V8 hidden-class optimizations win.