JavaScript Map vs Object: Key Differences, Pitfalls and a Benchmark
Both store key-value pairs, but they behave differently with key types, ordering, inherited properties and JSON. We tested each difference in Node.js, including a benchmark whose result may surprise you.
- Use an Object for records with fixed, known fields, such as a user or a config.
- Use a Map for dictionaries whose keys come from data, especially non-string keys or user input.
- Object keys are converted to strings; Map keys keep their type, including objects.
- In our Node.js benchmark, Objects were faster, so choose Map for correctness, not assumed speed.
You need to count how often each word appears in some text. An object literal, {}, seems like the obvious choice. Then a user types the word “constructor” and the count comes out as a block of function source code. That bug, which we reproduce below, is the clearest reason JavaScript has a separate Map type.
We ran every example in Node.js 24, and the outputs shown are from that run. Browsers behave the same way, though benchmark numbers will differ by engine and machine.
The short answer
| Feature | Object | Map |
|---|---|---|
| Key types | Strings and symbols | Any value, including objects |
| Inherited keys | Yes, from the prototype | None |
| Key order | Integer-like keys first, then insertion order | Insertion order |
| Size | Object.keys(o).length |
m.size |
| Iteration | Via Object.entries() |
Directly with for...of |
| JSON support | Built in | Needs conversion |
| Literal syntax | { a: 1 } |
None |
Basic usage
// Object
const o = {};
o.pen = 10;
o["book"] = 250;
delete o.pen;
"book" in o; // true
// Map
const m = new Map();
m.set("pen", 10).set("book", 250);
m.get("book"); // 250
m.has("pen"); // true
m.delete("pen");
Map’s methods are explicit, and set returns the Map, so calls can be chained. Objects use property syntax, which is shorter but mixes data with the object’s own properties, as we will see.
Key types
const o = {};
o[1] = "one"; o["1"] = "string one"; o[true] = "t";
console.log(Object.keys(o)); // [ '1', 'true' ]
const m = new Map();
m.set(1, "one").set("1", "string one").set(true, "t");
console.log([...m.keys()], m.size); // [ 1, '1', true ] 3
The object ended up with only two keys. The number 1 and the string "1" both became the string “1”, so the second assignment overwrote the first. The Map kept three distinct keys with their original types. If your keys are IDs that might arrive as numbers in one place and strings in another, an Object hides that inconsistency and a Map exposes it.
Objects as keys
const user = { id: 7 };
const cache = new Map([[user, "profile"]]);
cache.get(user); // 'profile'
cache.get({ id: 7 }); // undefined
A Map can use an object itself as a key, which is perfect for attaching data to DOM elements or request objects. Lookup is by identity: a different object with the same contents is a different key. An Object cannot do this at all; it would convert both to the string "[object Object]", so every object key would collide.
The prototype trap
Here is the word-count bug from the introduction:
const counts = {};
for (const w of ["constructor", "toString", "pen"]) {
counts[w] = (counts[w] || 0) + 1;
}
console.log(counts);
// constructor: 'function Object() { [native code] }1',
// toString: 'function toString() { [native code] }1',
// pen: 1
Every plain object inherits properties such as constructor and toString from Object.prototype. Reading counts["constructor"] returned the inherited function, the || kept it, and adding 1 turned it into a string. The same code with a Map produced the correct { constructor: 1, toString: 1, pen: 1 }.
The trap has a security side too. Parsing {"__proto__": {"isAdmin": true}} as JSON and copying it with Object.assign gave the result an inherited isAdmin of true in our test. Depending on how data is merged, this family of bugs, called prototype pollution, can let attackers change the behaviour of other objects. Validating input and using Maps for user-supplied keys removes the risk.
Tip: If you must use an object as a dictionary, create it with Object.create(null). It has no prototype, so nothing is inherited. In our test, setting toString on such an object simply stored the value 1. Use Object.hasOwn(obj, key) instead of in to check for keys.
Key order
const order = { b: 1, a: 2, 10: "x", 2: "y" };
Object.keys(order); // [ '2', '10', 'b', 'a' ]
const mo = new Map([["b", 1], ["a", 2], [10, "x"], [2, "y"]]);
[...mo.keys()]; // [ 'b', 'a', 10, 2 ]
Objects list integer-like keys first, in numeric order, then the remaining string keys in insertion order. Maps always keep insertion order. If order matters, for example in a “recently viewed” list or a simple cache that evicts the oldest entry, a Map behaves the way you expect.
Size and iteration
A Map knows its size: m.size is a property. An Object has to build a list of keys first: Object.keys(o).length. Both gave 2 for our two-entry examples, but the Object version creates a temporary array every time.
Maps are directly iterable:
for (const [key, value] of map) { ... }
map.forEach((value, key) => { ... });
With an object, you go through Object.entries(), as covered in our destructuring guide.
JSON
JSON.stringify({ a: 1 }); // '{"a":1}'
JSON.stringify(new Map([["a", 1]])); // '{}'
JSON.stringify(Object.fromEntries(new Map([["a", 1]]))); // '{"a":1}'
A Map serialises to an empty object, silently losing its data. That is a real bug risk in API responses and saved state. Convert with Object.fromEntries(map) before sending, and back with new Map(Object.entries(obj)) after receiving. This only works for string keys; Maps with object keys have no natural JSON form. This is one reason plain objects remain the standard for data sent over the network, as in our Fetch API guide.
Benchmark: is Map faster?
Many articles say Maps are faster for frequent additions and deletions. We measured one million string-key insertions and deletions, and ten million reads of a fixed key:
object add+delete 1364.3 ms
map add+delete 1577.7 ms
object read fixed 17.0 ms
map read fixed 85.9 ms
In this run, the Object was faster in both tests. Modern engines heavily optimise objects, especially for reading a known property, where the Object was about five times faster. Results depend on the engine, key types, data sizes and access patterns, so other workloads may favour Map. The honest conclusion is that you should not choose a Map for speed without measuring your own case. Choose it for correct behaviour with dynamic keys.
A benchmark like this is a single measurement, not a scientific result. For serious decisions, repeat runs, warm up the code and test on the runtime you deploy.
WeakMap
const wm = new WeakMap();
let el = { tag: "div" };
wm.set(el, { clicks: 3 });
wm.get(el); // { clicks: 3 }
wm.has({ tag: "div" }); // false
A WeakMap only accepts objects as keys and holds them weakly. When nothing else references the key object, it can be garbage-collected along with its entry. That makes WeakMaps ideal for caching or attaching private data to objects without causing memory leaks. The trade-off is that a WeakMap cannot be iterated and has no size, because entries can vanish at any time.
Which to choose
| Situation | Use |
|---|---|
| A record with known fields (user, config, API payload) | Object |
| Data that will be sent as JSON | Object |
| Counting or grouping by values from data | Map |
| Keys from user input | Map |
| Non-string keys, such as numbers or objects | Map |
| Ordered cache or frequent add/remove | Map |
| Data attached to objects without leaks | WeakMap |
In TypeScript, the same split applies: interfaces or types for records, and Map<K, V> for dictionaries. Our TypeScript tutorial covers typing both.
Testing tips
When testing code that builds dictionaries from user data, include awkward keys such as constructor, __proto__, toString, an empty string and numbers that look like strings. These inputs are exactly where Objects and Maps behave differently, and they rarely appear in happy-path test data. For a wider set of tricky inputs, see our guide to property-based testing, which generates them automatically.
Final thoughts
Objects are records; Maps are dictionaries. Use an Object when the shape is known in advance and the data may become JSON. Use a Map when keys come from data, when they are not strings, or when order and size matter. Do not pick Map for speed alone; in our test, plain objects were faster.
Frequently asked questions
What is the difference between Map and Object in JavaScript?
A Map accepts any key type, keeps insertion order, has a size property and inherits no keys. An Object converts keys to strings, inherits prototype properties and works directly with JSON.
Is Map faster than Object?
Not always. In our Node.js benchmark, Objects were faster for both add/delete and reads. Measure your own workload rather than assuming.
How do I convert a Map to JSON?
Use JSON.stringify(Object.fromEntries(map)). Stringifying a Map directly produces an empty object. This works for string keys only.
Can a Map use objects as keys?
Yes. Lookup is by identity, so the exact same object must be used to retrieve the value. An Object would convert object keys to the same string.
When should I use a WeakMap?
Use it to attach data to objects without preventing garbage collection, for example caching per DOM element. It cannot be iterated and has no size.
Keep learning
Need help with testing, WordPress or SEO?
We help teams ship better software and grow their sites. Tell us what you are working on and we will suggest a clear next step.