Map, Set, WeakMap and WeakSet

Use collections built for lookups and uniqueness, and attach data to objects without keeping them alive.

  • Beginner
  • 9 min read
  • Updated 17 Oct 2026

What you will learn

  • Choose Map over an object
  • Remove duplicates with Set
  • Cache results keyed by URL or object
  • Explain what WeakMap and WeakSet are for

Before you start

Objects and arrays can do almost anything, but two jobs come up so often that JavaScript has dedicated collections for them. A Map stores values by key, with any kind of key. A Set stores unique values. Their weak cousins, WeakMap and WeakSet, let you attach information to objects without stopping those objects from being cleaned up.

Map

A Map holds key-value pairs. Here the keys are course ids from the dashboard data, which are numbers:

JavaScript
const levels = new Map();

levels.set(1, "Beginner");
levels.set(2, "Intermediate");

console.log(levels.get(1)); // → Beginner
console.log(levels.has(2)); // → true
console.log(levels.size);   // → 2

levels.delete(2);
console.log(levels.size);   // → 1

console.log(levels.get(99));  // → undefined
console.log(levels.get("1")); // → undefined (the string "1" is a different key)
Method or propertyDoes
map.set(key, value)Adds or replaces an entry, and returns the map, so calls can be chained
map.get(key)Returns the value, or undefined
map.has(key)true if the key exists
map.delete(key)Removes the entry, returning true if it existed
map.clear()Removes every entry
map.sizeThe number of entries (a property, not a method)

You can also create a map from an array of [key, value] pairs:

JavaScript
const levels = new Map([
  [1, "Beginner"],
  [2, "Intermediate"],
]);

Any value can be a key

Object keys are always strings or symbols. A number key is quietly turned into a string, and an object key becomes the string "[object Object]". A Map keeps keys exactly as they are, using the same comparison as === (except that NaN matches NaN):

JavaScript
const map = new Map();
map.set(1, "number one");
map.set("1", "string one");
console.log(map.size); // → 2

const plain = {};
plain[1] = "number one";
plain["1"] = "string one";
console.log(Object.keys(plain).length); // → 1 (both keys became "1")

Objects as keys are especially useful. Here a map stores how many times each button was pressed:

JavaScript
const clicks = new Map();

document.addEventListener("click", (event) => {
  const button = event.target.closest("button");
  if (!button) return;
  clicks.set(button, (clicks.get(button) ?? 0) + 1);
});

Because keys are compared by identity, two different objects with the same contents are two different keys.

Iterating a Map

Maps remember insertion order and are directly iterable:

JavaScript
const lessonCounts = new Map([
  ["JavaScript", 54],
  ["CSS", 34],
  ["HTML", 25],
]);

for (const [topic, count] of lessonCounts) {
  console.log(`${topic}: ${count}`);
}
// → JavaScript: 54
// → CSS: 34
// → HTML: 25

console.log([...lessonCounts.keys()]);   // → [ 'JavaScript', 'CSS', 'HTML' ]
console.log([...lessonCounts.values()]); // → [ 54, 34, 25 ]

keys(), values() and entries() return iterators, so spread them into an array when you want array methods such as map or filter.

Converting between objects and maps

JavaScript
const settings = { theme: "dark", pageSize: 20 };

const asMap = new Map(Object.entries(settings));
const backToObject = Object.fromEntries(asMap);

console.log(backToObject); // → { theme: 'dark', pageSize: 20 }

This matters for JSON, because JSON.stringify(new Map([["a", 1]])) gives "{}". Convert a map with Object.fromEntries() (for string keys) or [...map] (for any keys) before saving it.

Counting with a Map

A common job for the dashboard: how many courses are there per topic?

JavaScript
const courses = [
  { id: 1, title: "JavaScript Basics", topic: "JavaScript" },
  { id: 2, title: "CSS Grid in Practice", topic: "CSS" },
  { id: 5, title: "Async JavaScript", topic: "JavaScript" },
];

const perTopic = new Map();
for (const course of courses) {
  perTopic.set(course.topic, (perTopic.get(course.topic) ?? 0) + 1);
}

console.log(perTopic); // → Map(2) { 'JavaScript' => 2, 'CSS' => 1 }

If you need the courses themselves grouped, Map.groupBy(courses, (c) => c.topic) returns a map from each topic to an array of its courses. It was added in ES2024 and is supported in all current browsers and in Node.js 21 and later.

Map or object?

Use a Map whenUse a plain object when
Keys are added and removed while the program runsThe set of keys is fixed and known in advance
Keys are not strings, such as objects or numbersYou are describing one thing with named properties
You need the number of entries oftenYou need to send or store it as JSON directly
Keys come from users or dataThe object will be destructured or spread

The last row on the left is a safety point. A plain object inherits properties from Object.prototype, so a key called "constructor" or "toString" already appears to exist. A Map starts truly empty.

A Map as a cache

The dashboard fetches data/courses.json, and later lessons will fetch more files. A Map keyed by URL makes a simple cache, so repeated requests reuse the first result:

JavaScript
const cache = new Map();

async function getCached(url) {
  if (cache.has(url)) return cache.get(url);

  const promise = fetch(url).then((response) => {
    if (!response.ok) throw new Error(`HTTP ${response.status} for ${url}`);
    return response.json();
  });

  cache.set(url, promise);

  try {
    return await promise;
  } catch (error) {
    cache.delete(url); // do not keep failures, so a retry can succeed
    throw error;
  }
}

Storing the promise rather than the data means that if two parts of the page ask for the same URL at the same moment, only one request is sent. Both callers await the same promise.

Set

A Set holds unique values. Adding a value that is already there does nothing:

JavaScript
const tags = new Set();

tags.add("javascript");
tags.add("async");
tags.add("javascript"); // already there, ignored

console.log(tags.size);          // → 2
console.log(tags.has("async"));  // → true
tags.delete("async");
console.log([...tags]);          // → [ 'javascript' ]

Like Map, a Set keeps insertion order, is iterable with for...of, and compares values like === (with NaN equal to itself).

Removing duplicates

The most common use is turning an array into one with no repeats:

JavaScript
const topics = ["JavaScript", "CSS", "JavaScript", "HTML", "CSS"];
const uniqueTopics = [...new Set(topics)];

console.log(uniqueTopics); // → [ 'JavaScript', 'CSS', 'HTML' ]

The dashboard can use exactly this to build the options for a topic filter from the course data.

Fast membership checks

array.includes(value) checks every element until it finds a match. set.has(value) is designed for fast lookups however large the set gets. When you check membership over and over, for example "is this course one of the user's saved courses?", keep the IDs in a set. This uses the courses array from the counting example:

JavaScript
const savedIds = new Set([2, 4, 6]);

const savedCourses = courses.filter((course) => savedIds.has(course.id));
console.log(savedCourses.map((c) => c.title)); // → [ 'CSS Grid in Practice' ]

Combining sets

ES2025 added methods for combining sets: union, intersection, difference, symmetricDifference, isSubsetOf, isSupersetOf and isDisjointFrom. They are supported in all current browsers and in Node.js 22 and later:

JavaScript
const frontEnd = new Set(["HTML", "CSS", "JavaScript"]);
const scripting = new Set(["JavaScript", "PHP"]);

console.log(frontEnd.intersection(scripting)); // → Set(1) { 'JavaScript' }
console.log(frontEnd.difference(scripting));   // → Set(2) { 'HTML', 'CSS' }

Where they are not available, a filter does the same job: new Set([...a].filter((x) => b.has(x))) is an intersection.

WeakMap and WeakSet

Normally, if an object is a key in a Map, the map keeps it alive. Even when the rest of the program has finished with it, the garbage collector cannot free it while the map still refers to it. For long-lived maps keyed by DOM elements or other temporary objects, that is a memory leak.

A WeakMap holds its keys weakly. If nothing else refers to a key object, it can be garbage collected, and its entry disappears with it.

JavaScript
const metadata = new WeakMap();

function trackRender(element) {
  metadata.set(element, { renderedAt: Date.now() });
}

function getRenderTime(element) {
  return metadata.get(element)?.renderedAt;
}

When an element is removed from the page and nothing else references it, its metadata goes too. You never have to remember to delete it.

The price of this behaviour is a smaller API:

  • Keys must be objects (or symbols that were not created with Symbol.for()). Strings and numbers are not allowed.
  • Only set, get, has and delete exist. There is no size, no clear and no iteration, because the contents can change whenever the garbage collector runs.

A WeakSet is the same idea for sets: it holds objects weakly and only offers add, has and delete. It suits marking objects, such as "this form has already been enhanced":

JavaScript
const enhanced = new WeakSet();

function enhanceForm(form) {
  if (enhanced.has(form)) return;
  enhanced.add(form);
  // ... add listeners and extra behaviour once
}

Reach for the weak versions when the keys are objects whose lifetime you do not control. Lesson 47 shows how to find leaks that a WeakMap would have prevented.

Summary

  • Map stores key-value pairs with keys of any type, keeps insertion order and has a size.
  • Prefer a Map for dynamic keys, non-string keys and lookups; prefer a plain object for fixed, named properties and JSON.
  • A Map keyed by URL, storing promises, makes a simple request cache.
  • Set stores unique values: [...new Set(array)] removes duplicates, and set.has() is a fast membership check.
  • Maps and sets do not survive JSON.stringify; convert them first.
  • WeakMap and WeakSet hold object keys weakly, so they do not prevent garbage collection, and they cannot be iterated or counted.

Further reading