Your Object Is Not a Hash Map: Seven Expressions Most People Get Wrong
Almost every JavaScript developer has been told to use a Map instead of a plain object when the
keys are data rather than code. Almost nobody does it, and the reason is that the advice always
arrives as a list of properties: any key type, insertion order preserved, no prototype.
Read as a list, none of those sounds like it would ruin an afternoon. Each of them ruins an afternoon in a specific way.
Predict, then find out
"Use a Map instead of an object for dictionaries" is advice most people have heard and few follow, because the reasons are always given as a list of properties — any key type, insertion order preserved, no prototype — and read as a list, none of them sounds like it would ruin an afternoon.
Each one ruins an afternoon in a specific way. An object's keys are always strings, so
obj[1] and obj["1"] are one key. Every object used as a key becomes
the string "[object Object]", so two entirely different objects collide silently.
Integer-like keys are not stored in insertion order — { b: 1, 2: 2, a: 3, 1: 4 } yields the keys ["1","2","b","a"]. And "toString" in is
true on an object you created a moment ago, because in walks the
prototype chain.
A Map has none of these behaviours: keys keep their type, keys are compared by
identity, insertion order is preserved for every key, and there is no prototype for anything to
leak through.
7 expressions. Predict what each evaluates to before you answer — the explanation only helps if you have already committed to being wrong.
- Score
- 0/0
- Question
- 1/7
- Answered
- 0
- Missed
- 0
The same round, as text
Expression
Your answer
Run so far
Answer before reading the explanation. Being wrong is the part that sticks.
What the runtime actually does
const obj = {};
obj[1] = "number one";
obj["1"] = "string one";
console.log("obj[1] and obj['1'] are:", Object.keys(obj).length, "key ->", JSON.stringify(obj));
const withOrder = { b: 1, 2: 2, a: 3, 1: 4 };
console.log("insertion order was b, 2, a, 1 — Object.keys gives:", JSON.stringify(Object.keys(withOrder)));
const m = new Map();
m.set(1, "number one");
m.set("1", "string one");
console.log("Map keeps them apart:", m.size, "keys ->", JSON.stringify([...m.keys()]));
console.log("Map keeps insertion order:", JSON.stringify([...new Map([["b",1],[2,2],["a",3],[1,4]]).keys()]));
const plain = {};
console.log("'toString' in {} is:", "toString" in plain);
console.log("Object.hasOwn({}, 'toString') is:", Object.hasOwn(plain, "toString"));
console.log("{}.toString is:", typeof plain.toString);
console.log("new Map().get('toString') is:", new Map().get("toString"));
const objKey = { id: 1 };
const same = { id: 1 };
const byRef = new Map([[objKey, "first"]]);
console.log("Map with an object key, looked up by an equal-but-different object:", byRef.get(same));
const asObj = {};
asObj[objKey] = "first";
console.log("Object with an object key becomes:", JSON.stringify(Object.keys(asObj)), "and asObj[same] is:", asObj[same]);obj[1] and obj['1'] are: 1 key -> {"1":"string one"}
insertion order was b, 2, a, 1 — Object.keys gives: ["1","2","b","a"]
Map keeps them apart: 2 keys -> [1,"1"]
Map keeps insertion order: ["b",2,"a",1]
'toString' in {} is: true
Object.hasOwn({}, 'toString') is: false
{}.toString is: function
new Map().get('toString') is: undefined
Map with an object key, looked up by an equal-but-different object: undefined
Object with an object key becomes: ["[object Object]"] and asObj[same] is: first Four failures, in the order they will cost you
Keys are coerced to strings. Storing under 1 and under "1" gives one key, not two. This is
harmless until you have a cache keyed by ids that arrive sometimes as numbers and sometimes as
strings from a query parameter, at which point two code paths quietly share one entry.
Every object used as a key becomes "[object Object]". Look at the last line: asObj[same]
returns "first" even though same is a completely different object that was never used as a key.
All objects collapse to the same key, so a lookup succeeds when it should fail. This is the one that
produces bugs nobody can reproduce, because the code reads correctly and the mistake is in the
language’s conversion rules rather than in the logic. A Map returns undefined here, which is the
right answer.
Integer-like keys are reordered. Inserting b, 2, a, 1 and reading the keys back gives
["1","2","b","a"]. Keys that look like array indices are moved to the front in ascending numeric
order, and only the remaining string keys keep insertion order. Anything that serialises an object
and expects stable ordering — a cache key, a signature, a diff — is resting on a rule with an
exception in it. Map has no exception.
The prototype is in the way. "toString" in {} is true on an object created a line earlier,
because in walks the prototype chain. Any membership test against keys from outside your program
needs Object.hasOwn, or it will report keys nobody inserted. Map has no prototype for anything to
leak through, which is the practical reason to reach for it whenever keys are user-controlled.
So when is a plain object still right?
Most of the time, and this page is not an argument that it never is.
Use an object when the keys are part of your program — fixed, known while writing the code, and few. A configuration object, a lookup table of handlers, the shape of a record. Object literals are shorter to write, serialise to JSON directly, and are what every API in the ecosystem expects.
Reach for a Map when the keys are data: user input, ids from a database, objects, anything
arriving at runtime that you did not choose. That is the line, and it is a much more usable rule
than “Map is better”.
What this page simplifies
Object.create(null)fixes the prototype problem. An object with no prototype has none of the inherited-key trouble, and is a real alternative when you want object syntax with Map-like isolation. It does not fix key coercion or ordering.- Symbols are the fourth key type. Objects accept string and symbol keys; symbol keys are not
coerced and are skipped by
Object.keys. That is a deliberate mechanism, not one of the traps here. - The ordering rule is specified, not accidental. Integer-like keys first in ascending order, then strings in insertion order, then symbols. It is defined behaviour and consistent across engines — surprising is not the same as unreliable.
- Map costs more. It is a real object with real overhead, it does not serialise to JSON without conversion, and it cannot be spread or destructured like an object literal. Those costs are why “always use Map” would be bad advice.