Module 4 · Objects, Classes & Data Structures ⏱ 19 min

Maps, Sets & Weak Collections

By the end of this lesson you will be able to:
  • Use a Map for key-value storage where keys can be any type and order is guaranteed
  • Use a Set to keep a collection of unique values and remove duplicates
  • Recognise when a plain object is the wrong tool and choose Map or Set instead

You have been using plain objects as dictionaries — string keys, looked up by name. That works for simple cases, but objects were designed to model things with properties, not to be general-purpose lookup tables, and the cracks show the moment you push them. Their keys are silently coerced to strings, they have no built-in way to tell you how many entries they hold, and the order of their keys is a historical accident rather than a promise you can lean on.

JavaScript ships two collections built specifically for the jobs objects do badly. A Map is a key-value store where the key can be anything — a string, a number, even another object — and insertion order is guaranteed. A Set is a collection that holds each value exactly once. Reach for them when you are storing and looking things up, and reserve plain objects for modelling actual things.

flowchart TD
  M["Map keys"] --> K1["strings"]
  M --> K2["numbers"]
  M --> K3["objects"]
  M --> K4["even functions"]
  O["plain object keys"] --> S["all coerced to strings"]
  style M fill:#1e7f3d,color:#fff
  style O fill:#b91c1c,color:#fff
A Map accepts any key type and keeps them distinct; a plain object forces every key into a string.

Map: a dictionary that plays fair

A Map is built around five methods you will use constantly. set(key, value) adds or updates an entry and returns the map, so calls chain. get(key) reads a value back (or undefined if absent), has(key) asks whether a key exists, delete(key) removes one entry, and size tells you how many entries there are — something a plain object makes you compute with Object.keys(obj).length.

Crucially, a Map never coerces its keys. Store a number key and it stays a number; store an object as a key and it is held by identity, so two different {} are two different keys. Insertion order is also preserved: when you loop over a Map, entries come out in the order you added them, every time.

set, get, has, delete, and size — the five moves you will use on every Map. Press Run.
const scores = new Map();
scores.set("Ada", 10);
scores.set("Grace", 7);
scores.set("Ada", 12);   // updates the existing key

console.log(scores.get("Ada"));
console.log(scores.has("Grace"));
console.log(scores.size);
scores.delete("Grace");
console.log(scores.size);

Set: each value, once

A Set is the right tool whenever 'unique' is part of the requirement. Calling add(value) inserts a value, but only if it is not already there — a second add of the same value is a no-op, so the set never holds duplicates. You get the same has, delete, and size that Map offers, and like Map it preserves insertion order.

The most common Set move is de-duplicating an array in one breath: new Set(arr) builds a set from the array (collapsing repeats), and spreading it back out with [...set] gives you a fresh array of the unique values, in the order they first appeared. No loop, no temp object, no indexOf check — that single expression is why Sets earn their place.

flowchart LR
  I["in: 3,1,4,1,5,5"] --> S["Set"]
  S --> O["out: 3,1,4,5 (unique)"]
  style S fill:#e0900b,color:#fff
  style O fill:#1e7f3d,color:#fff
A Set absorbs duplicates silently; spreading it back out yields the unique values in first-seen order.
A Set drops duplicates; spread it into an array for a clean unique list.
const seen = new Set();
[3, 1, 4, 1, 5, 9, 2, 6, 5].forEach((n) => seen.add(n));

console.log(seen.size);
console.log(seen.has(9));
const unique = [...seen];
console.log(unique);
Same idea, two outcomes: the object cache collides; the Map keeps object keys distinct.
const objCache = {};
objCache[{}] = 100;
objCache[{}] = 200;
console.log(objCache[{}]);        // 200 — both wrote the same string key

const map = new Map();
const a = {}, b = {};
map.set(a, 10).set(b, 20);
console.log(map.get(a));
console.log(map.get(b));
console.log(map.size);

Weak collections: keys that can vanish

There is a memory-minded cousin of each: WeakMap for key-value pairs and WeakSet for unique values. The 'weak' part means their keys (or values) must be objects, held so lightly that if nothing else in your program still references one, the garbage collector may reclaim it — and the entry quietly disappears from the collection on its own.

The price for that auto-cleanup is steep: a WeakMap has no size, no iteration, and no clearing, because its contents depend on garbage collection and so can never be enumerated. That makes WeakMap a specialist tool, usually for tucking private data against an object you do not own. For anything you need to count, look up by primitive, or loop over, a plain Map or Set remains the right choice — and the one you will reach for almost every day.

Exercise

What does this print? Track what set and get do, and remember set returns the map.

const m = new Map();
m.set("a", 1).set("b", 2);
console.log(m.get("b"));
console.log(m.size);
Exercise

Write charCount(str) that returns a Map mapping each character in str to how many times it appears. Use the character as the key; read its current count with get, defaulting to 0, then set the new count. So charCount("aba") returns a Map whose get('a') is 2 and get('b') is 1.

function charCount(str) {
  // return a Map of character -> count
}
Exercise

This memoizer caches a function's results by its argument, but it uses a plain object as the cache — and object keys are stringified, so two different object arguments both become "[object Object]" and collide, returning the wrong cached value. Replace the cache with a Map so object arguments keep their identity as keys (use has/get/set).

function memo(fn) {
  const cache = {};
  return function (arg) {
    if (cache[arg]) return cache[arg];
    const result = fn(arg);
    cache[arg] = result;
    return result;
  };
}
Exercise

Write unique(arr) that returns a new array of arr's values in first-occurrence order with duplicates removed. Do it with a Set: build a Set from arr (which collapses repeats) and spread it back into an array. So unique([1,2,2,3,1]) returns [1,2,3].

function unique(arr) {
  // return a new array with duplicates removed, order preserved
}
Exercise

You need to map each user object to its preferences. Why is a Map the right choice over a plain object?

Recap

  • A Map is a key-value store whose keys can be any type, kept by identity, with a real size, guaranteed insertion order, and set/get/has/delete.
  • A Set holds each value once; [...new Set(arr)] de-duplicates an array in one expression.
  • A plain object stringifies its keys, so number 1 and string "1" collide, and two object keys both become "[object Object]". That is the signal to switch to Map.
  • WeakMap and WeakSet hold object keys/values weakly so they can be garbage-collected, but they have no size and cannot be iterated — specialists for private data, not everyday lookups.

Next you'll learn destructuring, the concise syntax for pulling values out of exactly these objects and arrays.

Checkpoint quiz

What does new Set([1, 2, 2, 3]).size return?

Why do two different objects used as keys on a plain object overwrite each other?

Go deeper — technical resources