Two Collections Beyond Object and Array
Plain objects and arrays cover most of what JavaScript programs need, but they weren't designed for every job. Map and Set are built-in collections that fill two specific gaps: keyed lookups where keys aren't strings, and deduplicated membership checks.
They've been in the language since ES2015. Both are iterable, both have a .size property, and both work well with the spread operator. The mental model is simple:
Map— like an object, but keys can be anything and order is preserved.Set— like an array, but values are unique and lookup is fast.
Creating and Using a Map
A Map holds key/value pairs. You create one with new Map() and use .set(), .get(), .has(), and .delete():
You can also pass an array of [key, value] pairs to the constructor to seed it:
That two-element-array shape shows up everywhere Maps are involved — it's how entries are represented when you iterate.
Map vs Object: Why Bother?
Plain objects look like they do the same job. Most of the time they do. But Maps fix a few specific rough edges:
Objects inherit from Object.prototype, so keys like toString, constructor, and hasOwnProperty already exist on every object. Maps have no such baggage — the keys you set are the only keys that exist.
The other differences worth knowing:
- Any key type. Maps accept objects, functions, numbers, booleans as keys. Objects silently convert non-string keys to strings:
obj[1]andobj["1"]are the same slot. - Guaranteed insertion order. Maps iterate in the order entries were added. Objects mostly do too, but numeric-looking string keys get sorted first — a subtle trap.
- Built-in size.
map.sizeis O(1). For an object you'd writeObject.keys(obj).length, which rebuilds an array. - Optimized for churn. Engines tune Maps for frequent add/remove. Objects are tuned for stable-shape records.
Use an object when you're modeling a record with known string keys ({ name, email, age }). Use a Map when keys are dynamic, not strings, or when you'll add and remove entries a lot.
Iterating a Map
Maps are iterable, which means for...of works directly and destructuring each entry is natural:
If you want just keys or just values, call .keys() or .values(). And .forEach() exists if you prefer it:
To turn a Map back into a plain object or array, spread it:
Creating and Using a Set
A Set holds unique values. Adding a value that's already there is a no-op:
Uniqueness is determined with the same equality rule as ===, with one quirk: NaN is considered equal to itself inside a Set, even though NaN === NaN is false everywhere else.
Pass an iterable to the constructor to seed a Set — this is where the dedupe trick comes from:
One line, any primitive type. For arrays of objects this doesn't work — two different objects with the same fields are still two different values — but for strings, numbers, and booleans it's the idiomatic deduplication.
Set vs Array: When to Switch
Arrays and Sets both hold a collection of values, so when do you pick which?
Reach for a Set when:
- Values must be unique and you want the runtime to enforce it.
- You're doing lots of membership checks.
set.has(x)is O(1);array.includes(x)is O(n). Inside a loop, that difference compounds fast. - Order of insertion is all you need. Sets iterate in insertion order but don't support indexing.
Stick with an array when:
- You need positional access —
arr[0], slicing, sorting. - Duplicates are meaningful — a shopping cart with two of the same item.
- You'll use array methods like
.map,.filter,.reduceheavily. Sets don't have these; you'd spread into an array first.
A quick performance-shaped example:
If banned were an array, every filter callback would scan the whole list. As a Set, each lookup is constant time.
Iterating a Set
Same story as Map — for...of just works, and spreading gives you an array:
Sets also expose .keys(), .values(), and .entries() for symmetry with Map, even though for a Set keys and values are the same thing. Most of the time you'll just iterate directly.
A Worked Example: Counting Unique Visitors per Page
Combining both — a Map of page paths to a Set of visitor IDs:
The Map handles the path-to-bucket mapping; the Set handles the deduplication inside each bucket. Trying to do the same with a plain object and arrays would work, but you'd be writing extra indexOf checks and hasOwnProperty guards the whole way.
WeakMap and WeakSet, Briefly
Two related collections exist for a narrow use case: WeakMap and WeakSet. They hold references weakly, meaning an entry whose key (for WeakMap) or value (for WeakSet) has no other references gets garbage-collected automatically.
They only accept objects as keys, aren't iterable, and have no .size. That's by design — if you could iterate them, the garbage collector would be observable. They're useful for caching metadata about objects you don't own, and rare in day-to-day code.
Next: JSON
Map and Set are great in memory, but neither survives JSON.stringify intact — Maps become {} and Sets become {}. The next page covers JSON: how to serialize and parse data, and the patterns for handling the collections this page introduced when they need to cross a network or a file boundary.
Frequently Asked Questions
What's the difference between a Map and an Object in JavaScript?
A Map can use any value as a key — objects, functions, numbers, anything — while an object coerces keys to strings (or symbols). Map also tracks its size with .size, iterates in insertion order, and doesn't inherit keys from a prototype, so there's no risk of clashing with toString or constructor. Reach for Map when keys aren't strings or when you need frequent add/remove of entries.
What is a Set used for in JavaScript?
A Set stores unique values — it silently ignores duplicates. The fastest way to dedupe an array is [...new Set(arr)]. Sets also give you O(1) .has() checks, which beats array.includes() when you're testing membership inside a loop.
How do I iterate over a Map?
for...of works directly: for (const [key, value] of myMap) destructures each entry. You can also loop myMap.keys(), myMap.values(), or myMap.entries(). Iteration order is guaranteed to match insertion order, which plain objects don't always promise for numeric-looking keys.