skip to content

Keyed Collections

Before ES6 every lookup table was a plain object with string keys; now Map, Set, WeakMap, and WeakSet each fill a specific niche. Interviewers ask "why Map instead of an object?" to see whether you can name real differences — key types, ordering, size, and prototype safety — rather than repeating a preference.

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

explore

questions

13

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

In JavaScript, what does the expression [...new Set(myArray)] produce, and what are the limits of it as a deduplication idiom?

level: juniorimportance: must knowfreq 72%

basics

~20 s

Spreading an array into a Set and back yields a new array with duplicates removed and first-occurrence order preserved, because a Set stores each value only once. It collapses only values the Set treats as equal; distinct objects with identical contents survive.

open as a page

In JavaScript, how does a WeakMap differ from a Map, and what values are allowed as WeakMap keys?

level: juniorimportance: must knowfreq 68%

basics

~20 s

A WeakMap holds its keys weakly, so an entry becomes garbage-collectable as soon as nothing else references the key object. Keys must be objects (or unregistered symbols), and the API is only get, set, has and delete — no size, no iteration.

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

What equality rule does a JavaScript Set use to decide whether a value is already present, and what surprising results does it produce for NaN, -0, and objects?

level: middleimportance: must knowfreq 60%

basics

~20 s

A Set uses SameValueZero: like === except NaN counts as equal to itself, and +0 equals -0. So NaN deduplicates, 0 and -0 collapse into one entry stored as +0, and objects are matched by reference, never by their contents.

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

Why do JavaScript's WeakMap and WeakSet expose no size property and no way to iterate their contents?

level: middleimportance: should knowfreq 46%

basics

~20 s

Because their contents change whenever the garbage collector runs. Counting or enumerating them would make collection timing — an unspecified implementation detail that varies by engine and run — observable to programs, so the language exposes no way to look.

open as a page

How would you attach per-instance private data or metadata to objects you do not control — such as a frozen object or a third-party instance — and why not just set an extra property on the object?

level: middleimportance: should knowfreq 48%

basics

~20 s

Keep the data in a WeakMap keyed by the object, held in a scope only your module can reach. Nothing is added to the object, so frozen and third-party objects work, no name can collide, callers cannot see the data, and the entry dies with the object.

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

A service fetches a list of records and deduplicates it with new Set(records), but duplicates still get through. Explain why, and how you would deduplicate by content instead.

level: seniorimportance: should knowfreq 42%

basics

~20 s

A Set matches objects by reference, and every parsed record is a fresh object, so nothing collapses. Deduplicate on a derived primitive identity instead: keep a Set of ids, or build a Map keyed by that identity and take its values.

open as a page

You replace a leaking Map-based cache with a WeakMap keyed by the objects being cached. In which situations does the memory still not get released?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Whenever something else still reaches the key: a long-lived key object, another strong collection holding the same objects, or a value in the map that references a different key. WeakMap also cannot help at all when the cache is keyed by strings or numbers.

open as a page

How do you compute the union, intersection, and difference of two JavaScript Sets, both with the built-in Set methods and by hand?

level: middleimportance: nice to knowfreq 34%

basics

~10 s

Modern engines provide Set.prototype.union, intersection, difference and symmetricDifference, which return a new Set without mutating either operand. Before those existed the idioms were new Set([...a, ...b]) for union and [...a].filter(x => b.has(x)) for intersection.

open as a page

When would you reach for a JavaScript WeakSet, and what can you not do with one?

level: middleimportance: nice to knowfreq 32%

basics

~20 s

Use a WeakSet to mark objects — already visited, already validated, created by your factory — without keeping them alive or touching them. You get only add, has and delete: no size, no iteration, and members must be objects.

open as a page