Ch. 1 · JavaScript

JavaScript Map vs Object: When to Use Each (Interview Answer)

Map vs plain object for lookups: key types, insertion order, size, prototype keys like __proto__, JSON support and performance trade-offs.

~6 min readbeginnerupdated Oct 6, 2026

“When would you use a Map instead of a plain object?” This is one of the most common beginner-to-intermediate JavaScript interview questions, and it often arrives disguised as a coding task: “Count the word frequencies in this text,” or “Cache results keyed by the request object.” The interviewer wants to see whether you pick a data structure for a reason, and whether you know the specific ways a plain object can betray you when it is used as a dictionary.

The strongest answers distinguish two jobs: describing a record with known fields, and mapping arbitrary keys to values. Objects are built for the first; Map was added for the second.

Before you start

You should know object literal syntax, bracket access (obj[key]), Object.keys, and what a prototype is at a basic level (objects inherit properties from Object.prototype). Knowing what JSON serialization does is useful for the trade-offs section.

The short answer

Use a Map when keys are dynamic or come from user data, when keys are not strings (objects, numbers you do not want coerced), or when you add and remove entries frequently and need the count. Use a plain object for fixed-shape records whose fields you know when writing the code, and for data that goes straight to JSON. Objects convert every key to a string, inherit keys such as toString and __proto__ from their prototype, and have no size; Map keeps keys as-is, starts truly empty, preserves insertion order and reports size directly.

How it works

An object is a collection of properties. Property keys can only be strings or symbols, so anything else is converted with String(key). That includes objects, which all become "[object Object]".

const userA = { id: 1 };
const userB = { id: 2 };

const viaObject = {};
viaObject[userA] = 'Ada';
viaObject[userB] = 'Bo';
console.log(Object.keys(viaObject)); // [ '[object Object]' ]
console.log(viaObject[userA]); // Bo

const viaMap = new Map();
viaMap.set(userA, 'Ada').set(userB, 'Bo');
console.log(viaMap.get(userA), viaMap.size); // Ada 2

const nums = {};
nums[1] = 'one';
nums['1'] = 'string one';
console.log(Object.keys(nums)); // [ '1' ]
const numMap = new Map([[1, 'one'], ['1', 'string one']]);
console.log(numMap.size); // 2
JavaScript

Map compares keys with the SameValueZero algorithm: like ===, except NaN equals NaN. Objects are matched by identity, not by content, so two different { id: 1 } objects are two different keys.

Objects also come with inherited properties. A plain {} has Object.prototype behind it, so lookups for 'constructor' or 'toString' find something even though you never stored it.

const counts = {};
console.log('constructor' in counts); // true
console.log(typeof counts.toString); // function
const m = new Map();
console.log(m.has('constructor')); // false
JavaScript

Step-by-step walkthrough

Step 1: Ask what the keys are

If the keys are a fixed set of field names (name, email, role), you have a record: use an object or a class. If keys come from data (user ids, words in a document, request URLs, DOM nodes), you have a dictionary: lean towards Map. This question settles most cases before any performance talk.

Step 2: Count words safely

Word counting with an object looks fine until the text contains a word that matches an inherited property.

const text = 'the constructor called the builder';

const broken = {};
for (const w of text.split(' ')) broken[w] = (broken[w] || 0) + 1;
console.log(broken.constructor);
// function Object() { [native code] }1

const counts = new Map();
for (const w of text.split(' ')) counts.set(w, (counts.get(w) ?? 0) + 1);
console.log(counts.get('constructor'), counts.get('the')); // 1 2
console.log(counts.size); // 4
JavaScript

The object version read the inherited Object function, added 1 to it, and stored a string. The Map version cannot see inherited keys at all.

Step 3: Iterate and measure

A Map is directly iterable in insertion order and has size. Objects need Object.keys or Object.entries first, and their key order has a twist: integer-like keys are listed first in ascending order, then string keys in insertion order.

const obj = { b: 1, 2: 'x', a: 2, 1: 'y' };
console.log(Object.keys(obj)); // [ '1', '2', 'b', 'a' ]

const map = new Map([['b', 1], [2, 'x'], ['a', 2], [1, 'y']]);
console.log([...map.keys()]); // [ 'b', 2, 'a', 1 ]

for (const [key, value] of map) {
  if (key === 'a') console.log(key, value); // a 2
}
JavaScript

Step 4: Convert at the edges

JSON does not understand Map; JSON.stringify(new Map([['a', 1]])) produces {}. Convert at the boundary with Object.fromEntries and new Map(Object.entries(...)).

const prefs = new Map([['theme', 'dark'], ['lang', 'en']]);
console.log(JSON.stringify(prefs)); // {}
const json = JSON.stringify(Object.fromEntries(prefs));
console.log(json); // {"theme":"dark","lang":"en"}
const back = new Map(Object.entries(JSON.parse(json)));
console.log(back.get('theme')); // dark
JavaScript

Only do this when keys are strings; object keys cannot survive a round trip through JSON.

Worked scenario

An analytics endpoint counted page views per URL path with an object loaded from a database. A crawler requested /__proto__, and the stored count turned into nonsense, while a request for hasOwnProperty crashed a later check that called stats.hasOwnProperty(path).

const stats = {};
function track(path) {
  stats[path] = (stats[path] || 0) + 1;
}
track('/home');
track('__proto__');
track('hasOwnProperty');
console.log(Object.keys(stats)); // [ '/home', 'hasOwnProperty' ]
try {
  stats.hasOwnProperty('/home');
} catch (e) {
  console.log(e.message); // stats.hasOwnProperty is not a function
}
JavaScript

Assigning to __proto__ goes through an inherited setter rather than creating a key, so that count was simply lost. Overwriting hasOwnProperty with a number broke the method. Switching to Map fixes both, and gives a correct count of distinct paths for free.

const stats = new Map();
function track(path) {
  stats.set(path, (stats.get(path) ?? 0) + 1);
}
['/home', '__proto__', 'hasOwnProperty', '/home'].forEach(track);
console.log(stats.size); // 3
console.log(stats.get('__proto__'), stats.get('/home')); // 1 2
console.log(Object.fromEntries(stats));
// { '/home': 2, ['__proto__']: 1, hasOwnProperty: 1 }
JavaScript

If you must use an object as a dictionary, create it with Object.create(null) so it has no prototype, and use Object.hasOwn(obj, key) for checks.

Common mistake

  • “Map is always faster.” Engines optimise objects with a stable shape extremely well. Map tends to win for large dictionaries with frequent additions and deletions; for small fixed records, objects are as fast or faster. Measure before claiming either.
  • “Objects keep insertion order, so they are the same.” Integer-like keys jump to the front in numeric order.
  • Using map[key] = value on a Map. That sets an ordinary property on the Map object; get, has and size ignore it.
  • Expecting JSON.stringify to handle Map. It yields {} silently.
  • Expecting content equality for object keys. map.get({ id: 1 }) misses because it is a new object. Use a stable primitive key such as the id.

Verify the behavior

These assertions capture the differences an interviewer is listening for.

import assert from 'node:assert/strict';

const o = {};
o[{}] = 1;
assert.deepEqual(Object.keys(o), ['[object Object]']);

const m = new Map([[{}, 1], [{}, 2]]);
assert.equal(m.size, 2);

assert.equal('toString' in {}, true);
assert.equal(new Map().has('toString'), false);

assert.equal(new Map([[NaN, 'nan']]).get(NaN), 'nan');
assert.equal(JSON.stringify(new Map([['a', 1]])), '{}');
console.log('all assertions passed');
// all assertions passed
JavaScript

Save as map-vs-object.mjs and run node map-vs-object.mjs.

Follow-up questions

When would you choose WeakMap? When keys are objects and the entry should disappear once the key object is no longer referenced elsewhere, such as caching computed data per DOM node. WeakMaps are not iterable and have no size.

How do you deep compare two Maps? Check size, then for each [k, v] in one, confirm the other has(k) and the values are equal by your chosen rule.

Is Object.create(null) as good as a Map? It removes inherited keys, but keys are still coerced to strings, there is no size, and integer key ordering still applies.

Can you group with Map directly? Yes, Map.groupBy(items, fn) returns a Map from each key to an array of items, and accepts non-string keys.

Interview exercise

Write firstRepeated(list) that returns the first value which appears a second time, or undefined. The list can contain numbers, strings and object references, and 1 and '1' must be treated as different values.

Answer and reasoning

function firstRepeated(list) {
  const seen = new Map();
  for (const item of list) {
    if (seen.has(item)) return item;
    seen.set(item, true);
  }
  return undefined;
}
const shared = { id: 7 };
console.log(firstRepeated([1, '1', 2, 1])); // 1
console.log(firstRepeated([{ id: 7 }, { id: 7 }])); // undefined
console.log(firstRepeated([shared, 3, shared]) === shared); // true
JavaScript

A plain object would coerce 1 and '1' to the same key and would merge every object into '[object Object]', giving wrong answers. Map keeps the original key types and compares objects by identity, so two separate { id: 7 } literals are distinct while the same reference repeats. The loop is O(n) because Map lookups are effectively constant time. A Set would work equally well here since only membership matters; mentioning that is a bonus.

Continue learning

See more JavaScript interview questions and practise with the JavaScript MCQs. Related notes: Set deduplication and identity, WeakMap and object lifetimes and defending against prototype pollution. Official reference: Map on MDN.

More in JavaScript

read ✓JavaScript · easy

JavaScript Rest and Spread Gotchas

Where JavaScript's ... syntax surprises people: merge order, undefined overwriting defaults, lost getters and spreads that blow the stack.

~7 min readread →
esc