When would you choose a JavaScript Map over a plain object for a key-value store, and when is the plain object still the better choice?
answer
- record versus dictionary
- who writes the keys, you or the user
- what an empty object already answers to
- order, size, and any-value keys
- the wire format decides the rest
basics
~20 sUse a Map when keys are dynamic, non-string, or arbitrary user input, and when you need insertion-order iteration or a running count. Prefer a plain object for fixed, known string fields and for data that must serialize to JSON.
solid answer
~50 sI use a `Map` when the collection is a genuine dictionary: keys are dynamic or not strings, entries are added and removed at runtime, and I want to iterate them. `Map` accepts any value as a key — objects, functions, numbers — iterates every entry in insertion order, exposes `size` directly, and has no inherited keys, so a key like `'constructor'` or `'__proto__'` from user input cannot collide with anything. It also has a purpose-built API: `get`, `set`, `has`, `delete`, `clear`, plus `keys()`, `values()`, `entries()` and `forEach`. I stay with a plain object when the shape is fixed and known at write time — a record with named fields — or when the value has to round-trip through `JSON.stringify`, since a `Map`'s entries are invisible to JSON. Object literals also destructure and spread, which reads better for config-shaped data.
code
javascript · 16 linesconst permissions = new Map();
const alice = { id: 1, name: 'Alice' };
permissions.set(alice, ['read', 'write']); // object as key
permissions.set('*', ['read']); // string key alongside it
console.log(permissions.size); // 2
console.log(permissions.has(alice)); // true
for (const [who, perms] of permissions) {
console.log(who, perms); // insertion order, always
}
permissions.delete('*');
console.log(permissions.size); // 1
console.log(JSON.stringify(permissions)); // '{}' — JSON ignores entriesgo deeper
Know the headline differences: a Map takes any key type, keeps insertion order, and reports size, while an object is the natural choice for fixed named fields and for JSON.
Explain the mechanisms behind each difference — key stringification, inherited prototype keys, size versus Object.keys().length, and the get/set/has/delete API — and name the JSON limitation that pushes you back to objects.
Demonstrate the decision on real data: dynamic or externally supplied keys go into a Map, records stay objects, and conversion happens deliberately at the serialization boundary rather than scattered through the code.
Own the choice as an API contract: a Map in a public signature forces every consumer to convert for JSON and cloning, so decide once where the boundary sits and document which representation crosses it.
## Two different jobs A plain object is a *record*: a small, roughly fixed set of named fields you know while writing the code — `{ id, name, email }`. A `Map` is a *dictionary*: an open-ended set of entries whose keys arrive at runtime. JavaScript let objects do both jobs for years, which is why the question comes up: `Map` exists because the record type makes a mediocre dictionary. ## What Map gives you that an object does not **Any value as a key.** Object property keys are strings or symbols; anything else is converted to a string. `Map` keeps the key you passed, so objects, functions, numbers and booleans stay distinct keys, and object keys are matched by reference. ```js const meta = new Map(); const el = { tag: 'div' }; meta.set(el, { seen: 1 }); // keyed by the object itself meta.set(1, 'numeric key'); meta.set('1', 'string key'); // a separate entry ``` **Guaranteed insertion order for every key.** Iterating a `Map` — with `for...of`, `forEach`, or the `keys()`/`values()`/`entries()` iterators — visits entries in the order they were first inserted, no matter what the keys look like. Re-`set`ting an existing key updates the value and keeps its original position; `delete` then `set` moves it to the end. **A count, not a computation.** `map.size` is a property read. On an object you write `Object.keys(o).length`, which builds a throwaway array of every key just to count them. **No inherited keys.** A fresh `Map` is empty. A fresh object literal already responds to `'toString' in o`, `o.constructor`, and `o.valueOf`, because it inherits from `Object.prototype`. That is the single most common source of dictionary bugs when the keys come from outside your code. **A dedicated API.** `has` answers membership without confusing "missing" with "present but undefined"; `delete` is a method rather than an operator; `clear()` empties it in one call. Adding and removing entries is what `Map` is built for. **A cheap constructor from pairs.** `new Map(iterable)` accepts any iterable of `[key, value]` pairs, which pairs neatly with `Object.entries`, `array.map(...)`, or a generator. ## What the plain object still wins **JSON.** `JSON.stringify({ a: 1 })` gives `'{"a":1}'`; `JSON.stringify(new Map([['a', 1]]))` gives `'{}'`, because a Map's entries live in internal storage, not in enumerable properties. Anything crossing a network or a file boundary as JSON is usually simpler as an object. (Note that `structuredClone` *does* preserve a `Map` — it is JSON specifically that ignores it.) **Syntax and ergonomics.** Object literals, dot access, destructuring (`const { host, port } = config`), spread merging (`{ ...defaults, ...overrides }`) and optional chaining all read better for record-shaped data. A `Map` needs `get`/`set` calls everywhere. **Interop.** Vast amounts of code — the results of `JSON.parse`, options bags, anything you did not write — hand you plain objects. Converting to a `Map` at every boundary buys nothing when the keys are a handful of known strings. **Literal readability.** A config with five named settings is clearer as an object literal than as a `new Map([...])` with bracketed pairs. ## The decision in practice Ask what the keys are. Fixed, known, few, and written by you? Object. Dynamic, arbitrary, possibly non-string, possibly from user input, with entries coming and going? Map. Then ask where the data goes: if it must be JSON, either use an object or plan an explicit conversion (`Object.fromEntries` out, `new Map(Object.entries(...))` back in). A nuance worth voicing: the two are not mutually exclusive inside one program. It is normal to hold a `Map` as the live index in memory and convert to a plain object only at the serialization boundary. Choosing a `Map` for the working data does not commit you to an awkward wire format. One caution about performance claims. It is fair to say `Map` is designed for frequent insertion and deletion of arbitrary keys and that `size` avoids materialising a key array; it is not fair to recite benchmark numbers, which depend on the engine, the key distribution, and how the object was shaped. If asked, say you would measure the specific workload rather than assert a winner.
- Your API currently returns a plain object keyed by user id and you want to switch it to a Map — what breaks?Anything that serializes it. `JSON.stringify` on a Map yields `'{}'`, so a response body or a cache write silently empties. Property access, destructuring and spread also stop working, so every consumer has to move to `get`/`set`. If the Map is the right internal structure, keep it internal and convert with `Object.fromEntries` at the boundary.
- How do you decide whether a lookup should be keyed by an object or by an id string?Key by the object when callers already hold the exact instance and you want the entry to be about that instance — per-element metadata, for example. Key by an id string when lookups come from data that is re-parsed or re-created, because a structurally equal object is a different key and would miss.
- Does a plain object really not preserve insertion order?It preserves it only for ordinary string keys. Integer-like keys are visited first, in ascending numeric order, regardless of when they were added, so an id-keyed object iterates in an order you did not choose. A Map has one rule for every key type: insertion order.
saying these in an interview costs you the question
- Says Map is simply faster than an object, always
- Claims JSON.stringify serializes a Map's entries
- Thinks a fresh object literal has no keys at all
- Says objects preserve insertion order for every key type
- Believes Map can only be iterated after spreading it to an array