Viscosity
Review
Part I · JavaScript for engine builders · Day 2

8. Map and Set

Looking something up in an array means searching it. A Map finds the answer almost directly, whatever the key and however much it holds. The trick behind it, hashing, is one you’ll build yourself.

About 17 minutes, including the exercises and quiz
By the end you will be able to
  • Store and look up values by key with a Map, including object keys.
  • Keep a collection of unique values with a Set.
  • Loop over arrays, Maps and Sets with for...of.
  • Explain, in pictures, how a hash table finds a key without checking every entry.

Remembering things about objects

You want to note which particles have already been hit. An object seems like a good place to keep notes, with bracket access from chapter 3:

const hit = {};
const a = { id: 1 };
const b = { id: 2 };

hit[a] = true;
console.log(hit[b]); // true ?!

b was never hit, yet the answer is true. A property name is always a string, so JavaScript first turns a into a string, and every plain object turns into the same string: '[object Object]'. So hit[a] and hit[b] are the same property. The same thing happens with numbers: hit[1] and hit['1'] are one property too.

What you need is a lookup table whose keys can be anything, kept exactly as they are.

Intuition

A Map is a two-column table. On the left go the keys, on the right the values. You find a row by its key, and the key can be any value at all: a string, a number, even an object.

Map

A Map drawn as a table with a key column and a value column. The keys are a string, a number and an object.KEYVALUEKEY TYPE'ada'→36a string7→'seven'a number{ id: 1 }→100an object (by identity)
A Map: any key, one value each. Each row pairs a key with a value. The keys keep their own type: the string 'ada', the number 7 and an object are all different kinds of key.

Map is a class, so you make one with new and use its methods:

const ages = new Map();
ages.set('ada', 36);
ages.set('alan', 41);

console.log(ages.get('ada'));  // 36
console.log(ages.get('bob'));  // undefined
console.log(ages.has('alan')); // true
console.log(ages.size);        // 2
ages.delete('alan');

Object keys are compared by identity, exactly like === in chapter 4. So the particle problem goes away:

const hit = new Map();
hit.set(a, true);
console.log(hit.has(a));        // true
console.log(hit.has(b));        // false
console.log(hit.has({ id: 1 })); // false

The last line is a lookalike, a different object, so it isn’t found.

Definition

A Map holds key → value entries[1]. set(k, v) adds an entry, or replaces the value if k is already there. get(k) returns the value, or undefined if there is none. has(k) asks whether k is there, delete(k) removes it, and size counts the entries. Keys can be any value. They are compared like ===, so objects match only themselves. Each key appears at most once.

Here is the most common Map job of all, counting:

const cs = ['red', 'blue', 'red'];
const counts = new Map();
for (let i = 0; i < cs.length; i++) {
  const c = cs[i];
  if (counts.has(c)) {
    counts.set(c, counts.get(c) + 1);
  } else {
    counts.set(c, 1);
  }
}
console.log(counts);
// Map(2) { 'red' => 2, 'blue' => 1 }

for…of

Notice the counting loop only uses i to fetch cs[i]. When you don’t care about the index, there’s a shorter loop.

Intuition

A for loop with i says “for each slot number, look in that slot”. A for...of loop says “for each thing in here”. You get the things directly.

for (const c of ['red', 'blue']) {
  console.log(c);
}

for (const entry of counts) {
  console.log(entry[0], entry[1]);
}
// red 2
// blue 1

Looping over a Map gives you each entry as a two-slot array: [key, value]. Entries come out in the order their keys were first added[1]. You’ll often see this written for (const [k, v] of counts), a shorthand that unpacks the two slots into the names k and v.

Definition

for (const x of things) runs the body once for each value in things, in order, with x holding that value[3]. It works on arrays, strings, Maps and Sets. It doesn’t work on plain objects, and it doesn’t give you the index: if you need i, use the classic for loop.

Set

Sometimes there’s no value to store: you only want to know whether something is in the collection. That’s a Set.

const tags = new Set();
tags.add('fast');
tags.add('red');
tags.add('fast'); // already there

console.log(tags.size);       // 2
console.log(tags.has('red')); // true

const ids = new Set([3, 1, 3, 3, 2]);
console.log(ids); // Set(3) { 3, 1, 2 }
Definition

A Set is a collection of unique values[2]. add(v) puts v in, and does nothing if it’s already there. has(v) asks whether it’s there, delete(v) removes it, and size counts the values. It uses the same idea of “the same value” as Map keys, and new Set(array) is the quickest way to drop duplicates.

Try itMap, Set and for...of
const a = { id: 1 };
const b = { id: 2 };

const hp = new Map();
hp.set(a, 100);
hp.set(b, 40);
hp.set(a, 90);
console.log(hp.get(a), hp.size);
console.log(hp.get({ id: 1 }));

for (const e of hp) {
  console.log(e[0].id, e[1]);
}

const s = new Set([3, 1, 3, 2, 1]);
console.log(s, s.has(2));

How can a lookup be fast?

To find 'pink' in an array of a million names, a loop checks name after name, a million in the worst case. A Map gets there almost directly, however many entries it holds. How?

Intuition

Think of a post room with a wall of pigeonholes. Each letter’s address decides its pigeonhole. To find a letter, you don’t read every letter in the building: you work out which pigeonhole it belongs in, and look only there.

Step throughFinding a key without searching
// toy hash: (number of letters) % 4
m.set('red', 2);
m.set('blue', 1);
m.set('green', 5);
m.set('pink', 7);
m.get('green');
m.get('pink');
m.get('gold');
Four buckets numbered 0 to 3, each holding the entries whose key hashed to itAn empty table: 4 buckets.0empty1empty2empty3empty
A hash table is an array of buckets: here, 4 of them. To keep this readable, our toy hash function just counts the key’s letters, then takes % 4 to land on a bucket number.
1/8
Definition

Hashing, the intuition: a hash function turns a key into a number, and that number picks a bucket. Storing and finding a key both look only inside its bucket. When keys spread evenly across enough buckets, each bucket holds just a few entries, so a lookup stays quick however big the table grows. A table organised like this is a hash table.

Under the hood

The JavaScript standard doesn’t say how Map and Set must be built. It only requires that, on average, a lookup’s cost grows more slowly than the number of entries[1]. V8 builds Map, Set, WeakMap and WeakSet on hash tables. For an object key, V8’s hash code is simply a random number, unrelated to what the object contains, so it can’t be recomputed and has to be stored with the object[4]. That fits Map’s rule that objects are compared by identity, not contents.

You’ll build a hash table from scratch in chapter 51, and your virtual machine will keep its global variables in one. Engines use them for ordinary objects too: V8 switches an object’s property storage to a dictionary, a hash table, once it holds a great many properties[4], which you’ll meet in chapter 63. And compilers, Viscosity included, lean on lookup tables constantly: from each name to its declaration, from each value to what is known about it.

Your turn

ExerciseCount every value

Write countEach(xs). It returns a Map from each value in the array xs to how many times it appears. For example countEach(['a', 'b', 'a']) maps 'a' to 2 and 'b' to 1. The number 1 and the string '1' are different values and must be counted separately.

function countEach(xs) {
  // your code here
}

const m = countEach(['a', 'b', 'a']);
console.log(m);
Extra practiceThe first value you see twice

Write firstRepeat(xs). Read xs from left to right, and return the first value that you have already seen. If no value repeats, return undefined. For [3, 1, 4, 1, 5, 3] the answer is 1: you reach the second 1 (index 3) before the second 3 (index 5).

function firstRepeat(xs) {
  // your code here
}

const xs = [3, 1, 4, 1, 5, 3];
console.log(firstRepeat(xs));

What you learned

  • Object property names are always strings, so objects make poor lookup tables for non-string keys.
  • A Map stores key → value entries, with keys of any type, compared like ===. Use set, get, has, delete and size.
  • A Set stores unique values. Use add, has, delete and size.
  • for...of visits each value of an array, string, Map or Set, in order. Over a Map, each value is a [key, value] pair.
  • Hashing turns a key into a bucket number, so a lookup checks one bucket instead of everything.

That’s the end of Day 2. Next: functions that call themselves, and the stack that keeps track of every call.

Check your understanding

0/6
  1. 1.

    What does this print?

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

    What does this print?

    const m = new Map();
    m.set({ id: 1 }, 'hi');
    console.log(m.get({ id: 1 }));
  3. 3.

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

  4. 4.

    Why is a Map a better lookup table than a plain object when the keys are particle objects?

  5. 5.

    In for (const e of m), where m is a Map, what is e?

  6. 6.

    How does a hash table usually find a key without checking every entry?

References

  1. [1] MDN contributors. Map. MDN Web Docs. developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map (checked 2026-10-31)
  2. [2] MDN contributors. Set. MDN Web Docs. developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set (checked 2026-10-31)
  3. [3] MDN contributors. for...of. MDN Web Docs. developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...of (checked 2026-10-31)
  4. [4] Sathya Gunasekaran (2018). Optimizing hash tables: hiding the hash code. V8 blog (v8.dev), 29 January 2018. v8.dev/blog/hash-code (checked 2026-10-31)