GUIDE · JavaScript

Map & Set

lesson9 sectionsruns in browser35 asserts117 lines

At a glance

MAP
any key type, insertion order, .size, no prototype keys. Use it as your default dictionary in interviews.
SET
unique values, O(1) has/add/delete, insertion order.
OBJECT
keys become STRINGS; integer-like keys get reordered; inherits toString etc. Use Object.create(null) or Map if keys are user input.
TRAP
Map/Set compare objects/arrays BY REFERENCE — stringify compound keys.
SAY
hash mapO(1) average lookupfrequency mapseen setinsertion-orderedWeakMap for metadata without leaks

The idea#

Think of a Map like a coat check. You hand the clerk any token you like: a word, a number, or a whole object. They hang the coat and remember the token, and they remember who arrived first. size is how many coats are on the wall. A missed token comes back as undefined, which is why the counting line reads (counts.get(word) ?? 0) + 1. The ?? fills in 0 only when the hook is empty.

A Set is the clipboard at that same door. Each token is written once, in the order it first appeared. has, add, and delete are average O(1) lookups. That average comes from a hash map under the hood. add returns the set, so two adds can chain. Spread the set when you want an array of the tokens that remain.

A plain object is the row of mailboxes built into the same lobby. The label maker only prints strings. obj[1] = 'one' stores the key '1'. The mailbox labeled 1 and the mailbox labeled '1' are the same box. Labels that look like integers get lined up in numeric order when you list the keys, even if you inserted 2 before 1. Other labels keep insertion order. The wall also shipped with inherited mailboxes. 'toString' in obj is true for an ordinary {}, because the building has a prototype. Object.hasOwn asks whether you filed that label yourself. Object.create(null) builds a mailbox row with no prototype at all.

Why it matters#

Counting, grouping, and "have I already seen this" are half of the loops you will write by hand. A frequency map is the coat check of counts. A seen set drops duplicates and keeps first-seen order: [...new Set(xs)]. new Set(xs).size !== xs.length means the list had a repeat.

Grouping anagrams uses the sorted letters as the token, then pushes each word onto that token's list. Object.groupBy and Map.groupBy exist on newer engines. The loop with has and set runs everywhere, so it is the one to write when you do not know the runtime.

The trap is identity. Map and Set compare objects and arrays by reference. Two similar jackets are two tickets unless you hand over the same object. new Set([[1, 2]]).has([1, 2]) is false. Stringify a compound key, `${row},${col}`, or pack it into one number when the bounds are known, row * 100000 + col.

Insertion order is something you can lean on. An LRU cache deletes a key and sets it again to move that coat to the recent end, then evicts the first key when the size passes the cap. WeakMap and WeakSet accept only objects, skip enumeration, and let those objects be collected. Hang private metadata there. A different object with the same fields is a different key.

Reach for a Map when you add and remove often, when keys are not strings, when you need .size or a reliable order, or when the keys come from outside. Reach for an object when the shape is a fixed record, you are about to send JSON, or you want destructuring and dot access.

A small example#

JS
const hook = new Map();hook.set('wool', 'Ada');hook.set('linen', 'Grace'); function tally(tokens) {  const counts = new Map();  for (const token of tokens) {    counts.set(token, (counts.get(token) ?? 0) + 1);  }  return counts;} const visits = tally(['ada', 'grace', 'ada']);const door = new Set(['ada', 'grace', 'ada']);const seen = new Set();seen.add('1,2');const hit = seen.has('1,2');const miss = new Set([[1, 2]]).has([1, 2]);

hook.get('wool') is 'Ada', and hook.size is 2. The clerk kept arrival order. visits.get('ada') is 2. door.size is 2, because the clipboard writes a token once. hit is true: the key is the string you built. miss is false: each [1, 2] is its own array, and the set compares those arrays by reference.

What the code below is doing#

The code below is nine sections, from the counting idiom to the choice between Map and object.

Counting uses set, get with ?? 0, has, size, and delete (true the first time, false after). Iterating spreads keys, values, and entries, round-trips through Object.fromEntries, and reduces to the key with the largest count. Grouping files words by their sorted letters.

Set covers duplicates, chained add, spread order, uniq, and a size check for repeats. Intersection, difference, and union are filters and spreads, so they run without newer Set methods. Compound keys hit on the string '1,2', miss on a fresh [1, 2], and show a numeric packing trick. The LRU class moves a hit to the recent end with delete-then-set, and put evicts the oldest key once size passes the cap.

Plain objects show a numeric key stored as '1', ordinary labels in insertion order, integer-like labels sorted ascending, 'toString' in obj as an inherited hit, Object.hasOwn, Object.create(null), and an empty check with Object.keys. WeakMap stores metadata on a user object and misses a lookalike. The closing note is when you pick a Map and when you pick an object.

The trap: Map and Set compare objects and arrays by reference. Stringify a compound key, or the lookup misses a value that only looks the same. Say hash map, O(1) average lookup, frequency map, seen set, insertion-ordered, and WeakMap for metadata without leaks.

Everything below is the demonstration. Same idea, written as code you can run.

JSjavascript/map-set.js · setup
'use strict';const assert = require('node:assert/strict');
01

The counting idiom#

JS
const counts = new Map();for (const w of ['a', 'b', 'a', 'c', 'a']) counts.set(w, (counts.get(w) ?? 0) + 1);
Verified by 6 asserts
JS
assert.equal(counts.get('a'), 3);assert.equal(counts.get('zzz'), undefined);                  // miss -> undefined; hence ?? 0assert.equal(counts.has('b'), true);assert.equal(counts.size, 3);assert.equal(counts.delete('c'), true);assert.equal(counts.delete('c'), false);
02

Iterating / converting#

JS
for (const [k, v] of counts) assert.equal(typeof k + typeof v, 'stringnumber');const maxKey = [...counts].reduce((best, cur) => (cur[1] > best[1] ? cur : best))[0];
Verified by 6 asserts
JS
assert.deepEqual([...counts.keys()], ['a', 'b']);assert.deepEqual([...counts.values()], [3, 1]);assert.deepEqual([...counts], [['a', 3], ['b', 1]]);         // same as [...counts.entries()]assert.deepEqual(Object.fromEntries(counts), { a: 3, b: 1 });assert.deepEqual(new Map(Object.entries({ x: 1 })).get('x'), 1);assert.equal(maxKey, 'a');
03

Grouping#

JS
const words = ['eat', 'tea', 'tan', 'ate', 'nat', 'bat'];const groups = new Map();for (const w of words) {  const key = [...w].sort().join('');  if (!groups.has(key)) groups.set(key, []);  groups.get(key).push(w);}// Object.groupBy / Map.groupBy exist in Node 21+ — don't rely on them in an unknown runtime.
Verified by 1 assert
JS
assert.deepEqual([...groups.values()], [['eat', 'tea', 'ate'], ['tan', 'nat'], ['bat']]);
04

Set#

JS
const seen = new Set([1, 2, 2, 3]);seen.add(4).add(4);                                          // add returns the set (chainable)seen.delete(1);const uniq = xs => [...new Set(xs)];const hasDuplicates = xs => new Set(xs).size !== xs.length;// Set algebra (Node 22 has .union/.intersection; portable versions:)const A = new Set([1, 2, 3]), B = new Set([2, 3, 4]);
Verified by 8 asserts
JS
assert.equal(seen.size, 3);assert.equal(seen.has(2), true);assert.deepEqual([...seen], [2, 3, 4]);assert.deepEqual(uniq([3, 1, 3]), [3, 1]);assert.equal(hasDuplicates([1, 2, 1]), true);assert.deepEqual([...A].filter(x => B.has(x)), [2, 3]);                // intersectionassert.deepEqual([...A].filter(x => !B.has(x)), [1]);                  // differenceassert.deepEqual([...new Set([...A, ...B])], [1, 2, 3, 4]);           // union
05

Compound keys: by reference!#

JS
const visited = new Set();visited.add(`${1},${2}`);const pointKey = (r, c) => r * 100000 + c;                   // numeric key alternative if bounds are known
Verified by 3 asserts
JS
assert.equal(visited.has('1,2'), true);assert.equal(new Set([[1, 2]]).has([1, 2]), false);          // different array objectassert.equal(pointKey(1, 2), 100002);
06

Map as an LRU (insertion order + delete/re-set trick)#

JS
class LRU {  #cap; #m = new Map();  constructor(cap) { this.#cap = cap; }  get(k) {    if (!this.#m.has(k)) return -1;    const v = this.#m.get(k);    this.#m.delete(k); this.#m.set(k, v);                    // move to most-recent (end)    return v;  }  put(k, v) {    this.#m.delete(k);    this.#m.set(k, v);    if (this.#m.size > this.#cap) this.#m.delete(this.#m.keys().next().value);   // evict oldest (first)  }}const lru = new LRU(2);lru.put(1, 'a'); lru.put(2, 'b'); lru.get(1); lru.put(3, 'c');
Verified by 2 asserts
JS
assert.equal(lru.get(2), -1);assert.equal(lru.get(1), 'a');
07

Plain object pitfalls#

JS
const obj = {};obj[1] = 'one';const dict = Object.create(null);                            // no prototype at all
Verified by 7 asserts
JS
assert.equal(obj['1'], 'one');                               // key is the string "1"assert.deepEqual(Object.keys({ b: 1, a: 2 }), ['b', 'a']);           // insertion order...assert.deepEqual(Object.keys({ 2: 'x', 1: 'y' }), ['1', '2']);       // ...except integer-like keys sort ascendingassert.equal('toString' in obj, true);                       // inherited!assert.equal(Object.hasOwn(obj, 'toString'), false);         // Node 16.9+; or obj.hasOwnProperty.callassert.equal('toString' in dict, false);assert.equal(Object.keys({}).length, 0);                     // "is empty" check
08

WeakMap / WeakSet#

keys must be objects, not enumerable, GC-friendly (private data, caches)

JS
const meta = new WeakMap();const user = { id: 1 };meta.set(user, { lastSeen: 123 });
Verified by 2 asserts
JS
assert.equal(meta.get(user).lastSeen, 123);assert.equal(meta.has({ id: 1 }), false);                    // different object
09

Map vs Object: when the interviewer asks#

JS
// Map: frequent add/remove, non-string keys, need size, need order, keys from user input.// Object: fixed shape records, JSON in/out, destructuring, property access syntax.

See also