Command Palette

Search for a command to run...

Hectal
PHASE 14Advanced ES2015+ ~26 min· topic 3 of 9

Topic 14.3

Map and Set

In one line

Map is a key-value collection whose keys can be any value (objects included) and which remembers insertion order; Set is a collection of unique values. Both are iterable, have a real size, and are built for frequent adding, looking up and removing.

Think of it like this

A coat check and a guest list. At a coat check you hand over a coat (the key) and get a ticket back pointing to its hook (the value); any kind of coat works, and the coat itself is the key. That's a Map. A guest list only records who is coming; writing a name twice doesn't make two guests. That's a Set.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Map
A collection of key-value pairs where the key can be any value, kept in the order you added them.
Set
A collection where each value appears at most once.
Entry
One key-value pair in a Map, written as a two-item array [key, value].
By reference
Two objects count as the same key only if they are the very same object, not just alike.
SameValueZero
The comparison Maps and Sets use: like ===, but NaN matches NaN.
Insertion order
The order in which items were added. Maps and Sets loop in this order.
Union / intersection
Everything in either set / only what is in both sets.
Difference
What is in the first set but not in the second.

Step by step

01Create and fill a Map

Start empty and set entries, or pass an array of pairs. set returns the map, so calls chain.

prices.jswhole filejavascript
const prices = new Map([
  ["tea", 30],
  ["samosa", 15],
]);
prices.set("juice", 50).set("tea", 35); // tea replaced
console.log(prices.get("tea"), prices.size); // 35 3

02Use objects as keys

Attach data to an object without touching it, for example how many times each DOM element was clicked. With a plain object as the store, every element would become the same key "[object Object]".

clicks.jswhole filejavascript
const clicks = new Map();
for (const button of document.querySelectorAll("button")) {
  button.addEventListener("click", () => {
    clicks.set(button, (clicks.get(button) ?? 0) + 1);
  });
}

03Count and group

Counting with a Map is a pattern you'll use in /dsa constantly (frequency maps, two-sum, anagrams): counts.set(x, (counts.get(x) ?? 0) + 1).

04Remove duplicates with a Set

Pass any iterable to new Set() and spread it back to an array. Order of first appearance is kept.

unique.jswhole filejavascript
const unique = [...new Set([3, 1, 3, 2, 1])];
console.log(unique); // [ 3, 1, 2 ]

05Combine sets

The ES2025 methods read like the maths. Each returns a new Set.

Combine setsdiagram
Rendering diagram…

06Choose the right collection

Fixed record with string keys → object. Growing dictionary, non-string keys, order or size → Map. Ordered list with duplicates → array. Membership checks and uniqueness → Set.

Try it yourself

  1. 1

    Find the object key again

    In the first example, m.get({ id: 1 }) gave undefined. Change it so it finds "an object" without changing the Map. (Hint: which variable holds the exact object?)

  2. 2

    Count letters

    In the second example, count the letters of "banana" instead of words. Predict the printed Map before you run it: what order will the keys be in?

  3. 3

    Which tags are new?

    In the third example, make const old = new Set(["js", "css"]) and log the tags that are in tags but not in old. Which method do you need?

Code & diagrams

Map keys of any type, and the object trap ES2015+ New tab

`{ id: 1 }` written again is a new object, so the Map doesn't find it. In the plain object, `1` became `"1"` and both objects became `"[object Object]"`, so `b` overwrote `a`.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

4
number one | string one
an object
undefined
Map(3) { '1' => 'string one', 1 => 'number one', { id: 1 } => 'an object' }
[ '1', '[object Object]' ]
object b
Looping, converting, counting and grouping New tab

`Map.groupBy` arrived in ES2024; the rest is ES2015. Everything comes out in insertion order.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

[ 'tea', 'samosa', 'juice' ]
{ tea: 30, samosa: 15, juice: 50 }
Map(2) { 'a' => 1, 'b' => 2 }
Map(5) { 'the' => 3, 'cat' => 1, 'and' => 2, 'hat' => 1, 'bat' => 1 }
[ 'Pune', 'Delhi' ]
[ 'Asha', 'Meera' ]
Sets: uniqueness and set operations ES2025+ New tab

The set operation methods (`union` and friends) are ES2025; the last line is the older way to intersect. Two look-alike objects are still two values.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

Set(3) { 'js', 'css', 'html' }
3 true
[ 3, 1, 2 ]
Set(2) { NaN, 0 }
2
Set(5) { 1, 2, 3, 4, 5 }
Set(2) { 3, 4 }
Set(2) { 1, 2 }
Set(3) { 1, 2, 5 }
true true
Set(2) { 3, 4 }

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Use brackets instead of set()

Write const m = new Map(); m["a"] = 1; console.log(m.get("a"), m.size);

terminal
$ Console
── what you'll see ──
undefined 0

Break #2

Send a Map as JSON

Write console.log(JSON.stringify(new Map([["a", 1]])));

terminal
$ Console
── what you'll see ──
{}

Myth vs fact

Myth

A Set removes duplicate objects that look the same.

Fact

Objects are compared by reference. new Set([{ a: 1 }, { a: 1 }]) has size 2. To dedupe records, key them by an id in a Map.

Myth

Maps are always faster than objects.

Fact

Maps are designed for frequent additions and deletions and large dynamic key sets. For small records with fixed shapes, engines optimise objects very well. Pick by meaning first.

Myth

Object keys keep the order you add them, so objects are as good as Maps for order.

Fact

Objects list integer-like keys first in ascending order, then string keys in insertion order. Maps keep pure insertion order for every key type.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    The spec requires Map and Set operations to be sublinear on average; engines use hash tables. V8 uses a deterministic ordered hash table (after Tyler Close's design), which keeps insertion order cheap and makes iteration fast.

  • ▸

    Deleting from a Map or Set while iterating it is safe and well defined: deleted entries that haven't been visited are skipped, and entries added during iteration are visited. Arrays have no such guarantee for splice inside forEach.

  • ▸

    Set methods accept any set-like argument (an object with size, has and keys), so a.intersection(map) works with a Map's keys. They do not accept arrays; wrap them with new Set(array) first.

  • ▸

    Maps don't keep -0: map.set(-0, x) stores the key as +0, and [...map.keys()] gives 0. This is the one place SameValueZero shows a difference from Object.is.

Remember this

  1. 1

    A Map stores entries (key-value pairs). new Map() makes an empty one; map.set(key, value) adds or replaces (and returns the map, so calls can chain); map.get(key) reads (or undefined); map.has(key), map.delete(key), map.clear() and the map.size property do what they say. You can start it from any iterable of pairs: new Map([["tea", 30], ["pen", 15]]), or new Map(Object.entries(obj)).

  2. 2

    The big difference from an object: keys of any type. Object property keys are always strings (or symbols): obj[1] and obj["1"] are the same property, and using an object as a key turns it into the string "[object Object]", so every object collides. A Map keeps keys as they are: 1 and "1" are different keys, and an object key is matched by reference (the exact same object, not one that looks the same). Maps also have no inherited keys, so a key called "__proto__" or "toString" is safe.

  3. 3

    Keys are compared with SameValueZero: like ===, except NaN equals NaN (so NaN works as a key), and 0 and -0 count as the same. Maps iterate in insertion order. for (const [key, value] of map) loops over entries, and map.keys(), map.values(), map.entries() and map.forEach((value, key) => ...) give the parts. Convert back with Object.fromEntries(map) or [...map]. Map.groupBy(items, fn) (ES2024) groups an array into a Map.

  4. 4

    A Set stores unique values, compared with the same SameValueZero rule. set.add(v), set.has(v), set.delete(v), set.clear(), set.size. Adding a value that's already there does nothing. The most famous use is removing duplicates: [...new Set(array)]. Two different objects with the same contents are still two values, because objects are compared by reference.

  5. 5

    Sets now have set operations built in (ES2025): a.union(b) (in either), a.intersection(b) (in both), a.difference(b) (in a but not b), a.symmetricDifference(b) (in exactly one), plus the yes/no checks a.isSubsetOf(b), a.isSupersetOf(b) and a.isDisjointFrom(b) (no values in common). Each returns a new Set and doesn't change a. In older code you'll see the same done with spread and filter: new Set([...a].filter((x) => b.has(x))).

  6. 6

    When to use which: use an object for a record with fixed, known string keys (a user's name, email), and for JSON. Use a Map for a dictionary that grows and shrinks at runtime, has non-string keys, or needs order and size. Use an array for an ordered list where duplicates and positions matter, and a Set when you mostly ask "is this in it?": set.has(x) is fast on average however big the set gets, while array.includes(x) checks items one by one. One catch: JSON.stringify turns a Map or Set into {}, so convert first.

Explain it without notes

01

When would you choose a Map over a plain object?

02

How do Map and Set compare keys and values? What happens with NaN and with objects?

03

How do you remove duplicates from an array, and what are the limits of that method?

04

Name the ES2025 set methods and say what each returns.

Practice

01

Write firstRepeat(list) that returns the first value that appears a second time, using a Set. firstRepeat(["a", "b", "c", "b", "a"]) should return "b".

02

Given const orders = [["tea", 2], ["pen", 1], ["tea", 3]], build a Map of total quantity per item and log it.

03

Two classes share some students: const x = new Set(["Asha", "Ravi", "Meera"]) and const y = new Set(["Ravi", "Kabir"]). Log the students in both, and everyone in either.

Trade-offs

  • ↔

    Maps and Sets give any-type keys, real order and fast membership, but they don't serialise to JSON and don't work with object destructuring or dot access, so they cost a conversion at API boundaries.

  • ↔

    array.includes is fine for a handful of items and keeps duplicates and order; a Set pays a little memory and setup time to make repeated has checks fast on large collections.

Done when you can

  • I can create, read, update, delete and loop over a Map.

  • I can explain why objects make poor dictionaries for non-string keys.

  • I can dedupe an array with a Set and know its limit with objects.

  • I can use union, intersection, difference and the subset checks.

  • I can convert between Map, object, array and JSON.

  • I can choose between object, Map, array and Set for a job.