skip to content

Map vs Plain Object

Map accepts any value as a key, remembers insertion order, reports its size in O(1), and carries no inherited prototype keys — all things a plain object gets wrong or does by accident. Knowing when the object's JSON-friendliness still wins is the second half of the answer.

part ofJavaScriptoverview, primer and where to startread it →
on this pageshow

questions

4

What does this JavaScript log, and why: `const o = {}; o[1] = 'a'; o['1'] = 'b'; const m = new Map(); m.set(1, 'a'); m.set('1', 'b'); console.log(Object.keys(o).length, m.size);`

level: juniorimportance: must knowfreq 58%

answer

  1. property keys are strings or symbols
  2. the index gets converted first
  3. two writes, one slot
  4. Map stores the key untouched
  5. every object stringifies the same way

basics

~20 s

It logs 1 and 2. Plain-object property keys are always strings or symbols, so o[1] and o['1'] are the same property. Map stores keys exactly as given and compares them without coercion, so the number and the string are separate entries.

solid answer

~40 s

It prints `1 2`. When you index a plain object with a non-symbol value, the engine converts that value to a string first, so `o[1]` and `o['1']` address one and the same property — the second write just overwrites the first, and `Object.keys(o)` gives `['1']`. A `Map` does no such conversion: it keeps each key as the value you passed and matches lookups by value identity, so `1` and `'1'` are two independent entries and `m.size` is 2. The same rule is why `o[{ id: 7 }]` becomes the property `'[object Object]'` — every object key collapses to one string — while a `Map` can hold many distinct objects as keys and matches them by reference, so `m.get({ id: 7 })` on a freshly built object returns `undefined`.

code

javascript · 13 lines
javascript
const o = {};
o[1] = 'a';
o['1'] = 'b';
o[{ id: 7 }] = 'c';
console.log(Object.keys(o)); // [ '1', '[object Object]' ]

const m = new Map();
m.set(1, 'a');
m.set('1', 'b');
m.set({ id: 7 }, 'c');
console.log(m.size);           // 3
console.log(m.get(1));         // 'a'
console.log(m.get({ id: 7 })); // undefined

go deeper

for a junior

Be able to say plainly that plain-object keys are strings, so o[1] and o['1'] are the same property, and that a Map keeps 1 and '1' apart.

for a middle

Explain the conversion step: indexing a plain object converts the key to a string unless it is a symbol, which is why every object key becomes '[object Object]', while Map matches keys by value identity and objects by reference.

for a senior

Show where the coercion produces real bugs — id lookups fed by both numeric and string ids, keys read back as strings and compared with === — and describe normalising keys at the boundary or moving to a Map.

for a principal

Frame it as an interface decision: a plain-object dictionary imposes a string key namespace on every caller, so pick and document one key type at the module boundary rather than letting each call site coerce differently.

## The rule behind the output A property key in JavaScript can only be a string or a symbol. Nothing else. When you write `o[k]`, the engine takes `k` and, unless it is already a symbol, converts it to a string before touching the object. So a numeric key, a boolean, `null`, or a whole object all become string property names. ```js const o = {}; o[1] = 'a'; o['1'] = 'b'; console.log(Object.keys(o)); // [ '1' ] — one property, value 'b' ``` A `Map` is a different kind of collection. Its entries are held in internal storage, not as properties, and the key is stored as the value you handed over — number stays number, object stays that object. Lookups compare keys by value identity rather than by string name, so nothing collapses together. ```js const m = new Map(); m.set(1, 'a'); m.set('1', 'b'); console.log(m.size); // 2 console.log(m.get(1)); // 'a' ``` Hence `1 2`. ## What happens with object keys Using an object as a plain-object key is the more dramatic version of the same trap. Converting an ordinary object to a string yields `'[object Object]'`, so every distinct object you use as a key lands on that single property: ```js const a = { id: 1 }; const b = { id: 2 }; const o = {}; o[a] = 'first'; o[b] = 'second'; console.log(Object.keys(o)); // [ '[object Object]' ] console.log(o[a]); // 'second' — b overwrote a ``` With a `Map`, `a` and `b` are two different keys, because object keys are matched by reference — the very same object, not one that merely looks alike: ```js const m = new Map([[a, 'first'], [b, 'second']]); console.log(m.size); // 2 console.log(m.get(a)); // 'first' console.log(m.get({ id: 1 })); // undefined — a different object ``` That last line is the counterpart trap: a `Map` will not find an entry from a structurally equal but freshly constructed key. If you want lookup by content, derive a primitive key yourself (an id, a canonical string) and use that. ## Symbols are the exception Symbols are legal property keys and are not stringified, so `o[Symbol('x')]` really does create a symbol-keyed property. But symbol keys do not show up in `Object.keys`, `JSON.stringify`, or a `for...in` loop, which is a separate reason not to rely on them for dictionary storage. ## Where this bites in real code The common bug is an id-keyed lookup built from mixed sources. Ids that arrive as numbers from one code path and as strings from another silently unify inside a plain object, so a cache appears to work until two "different" ids collide — or until you read the keys back and discover every one of them is now a string: ```js const byId = {}; byId[42] = { name: 'Ada' }; console.log(typeof Object.keys(byId)[0]); // 'string' ``` Any code that later compares `Object.keys(byId)[0] === 42` fails, because the key came back as `'42'`. With a `Map` keyed by the number 42, `[...byId.keys()][0] === 42` holds, since the key was never converted. ## How to avoid it Three practical moves. First, normalise: pick one key type and coerce at the boundary (`String(id)` or `Number(id)`) so mixed sources cannot diverge. Second, if the keys are genuinely not strings — objects, functions, numbers you want to stay numbers — use a `Map`, which is exactly the collection for that. Third, when you must use a plain object, remember that its keys are a string namespace: reading them back gives strings, and anything you can turn into the same string is the same slot. The interviewer's real target here is whether you know that object property keys are a *string* namespace, not an arbitrary-value one, and that `Map` was added to the language precisely to lift that restriction.

  • If a Map can hold objects as keys, why does `m.get({ id: 7 })` return undefined right after `m.set({ id: 7 }, 'c')`?
    Because those are two different objects. Map matches object keys by reference, not by structure, so a freshly built literal with identical contents is a different key. Either keep a reference to the original object and reuse it, or key the Map by a primitive you derive from the object, such as its id.
  • Are there any key values that a plain object does not turn into a string?
    Symbols. They are legal property keys and are stored as themselves, so two symbols with the same description are still distinct properties. The catch is that symbol-keyed properties are skipped by `Object.keys`, `for...in`, and `JSON.stringify`, so they are a poor fit for a general-purpose dictionary.
  • How does the engine decide what string an object key becomes?
    It converts the key to a primitive first and then to a string, which for an ordinary object goes through the inherited `toString` and yields `'[object Object]'`. A class that defines its own `toString` returning a unique id would therefore produce distinct property names — but that is fragile compared with just using a Map.

saying these in an interview costs you the question

  • Says objects can have numeric keys stored as numbers
  • Claims Map compares object keys by their contents
  • Thinks o[1] and o['1'] are different properties
  • Believes Object.keys returns numbers for numeric keys
  • Says using an object as an object key throws

context

open as a page

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?

level: middleimportance: must knowfreq 78%

basics

~20 s

Use 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.

open as a page

How do you convert a JavaScript Map to a plain object and back, and what does `JSON.stringify(new Map([['a', 1]]))` produce?

level: middleimportance: should knowfreq 48%

basics

~10 s

Object.fromEntries(map) builds a plain object from a Map, and new Map(Object.entries(obj)) converts back. JSON.stringify on a Map produces '{}' because its entries live in internal storage, not in own enumerable properties.

open as a page

A JavaScript service counts events with `counts[key] = (counts[key] || 0) + 1` on an object literal, where `key` comes from user input. What can go wrong, and how would you make it safe?

level: seniorimportance: should knowfreq 44%

basics

~20 s

An object literal inherits properties from Object.prototype, so keys like 'toString' or 'constructor' read back as inherited functions instead of undefined, and assigning 'proto' changes the object's prototype rather than adding an entry. Use a Map, or Object.create(null).

open as a page