Frontend / JavaScript core / data-structures.md

Data structures

Updated 6 interview angles 5 min read source
On this page7
  1. Map or object
  2. Set, and set operations
  3. WeakMap and WeakSet: the memory story
  4. Stack and queue, without a class
  5. Choosing, quickly
  6. Related
  7. Interview angle

Data structures

Object, Array, Map, Set, WeakMap, WeakSet. The interview question is almost never “what is a Map” — it is why you would choose one over a plain object, and that has real answers.

Map or object

js
const m = new Map();
m.set(userObj, "metadata");     // any value as a key
m.set(1, "number one");
m.get("1");                     // undefined — 1 and "1" stay distinct
m.size;                         // O(1)
Object Map
Key types string, symbol anything, including objects
Key coercion numbers become strings none
Size Object.keys(o).length .size
Order integer-like keys first, then insertion insertion, always
Inherited keys yes, from the prototype none
JSON direct needs conversion

Reach for a Map when keys are dynamic, non-string, or added and removed often. Reach for an object when the shape is known, when it is a record you will serialise, or when you want the ergonomics of dot access.

The key-order rule surprises people:

js
const o = { b: 1, 2: 2, a: 3, 1: 4 };
// ["1", "2", "b", "a"]  — integers sorted first
Object.keys(o);

If insertion order matters, that alone is the reason to use a Map.

Gotcha: an object literal inherits from Object.prototype, so a user-supplied key called constructor or __proto__ is already “present”. Object.create(null) or a Map is the safe container for untrusted keys — this is the prototype-pollution shape in Frontend Security — Senior Interview Prep.

Set, and set operations

js
const s = new Set([1, 2, 2, 3]);       // {1, 2, 3}
s.has(2);                              // O(1), vs O(n) for array.includes
[...new Set(items)];                   // the dedupe idiom

ES2025 added real set methods, which replace the manual filter/spread versions:

js
a.union(b);
a.intersection(b);
a.difference(b);
a.symmetricDifference(b);
a.isSubsetOf(b);
a.isDisjointFrom(b);

Membership is the point. Checking includes inside a loop is the classic accidental O(n²); building a Set first makes it O(n).

Both Map and Set key by SameValueZero, so NaN equals NaN — which is why new Set([NaN, NaN]).size is 1, and why Set finds a NaN that indexOf cannot.

WeakMap and WeakSet: the memory story

Keys must be objects, and they are held weakly — an entry does not stop its key being garbage collected:

js
const meta = new WeakMap();

function attach(el, data) {
  // no leak: when `el` goes, so does the entry
  meta.set(el, data);
}

With a normal Map, that same code keeps every element alive forever, because the Map holds a strong reference. That is the entire use case: associating data with an object you do not own the lifetime of — DOM nodes, class instances, request objects.

The trade is that they are not enumerable and have no size. You cannot iterate a WeakMap, because what it contains could change whenever the collector runs. That is not a limitation to work around; it is what makes the guarantee possible.

Stack and queue, without a class

Both are one line of array usage, and the reason to write them out is that one of them is a performance trap:

js
const stack = [];
stack.push(x);  stack.pop();        // both O(1)

const queue = [];
// shift is O(n) — reindexes everything
queue.push(x);  queue.shift();

shift() on a large array moves every remaining element. For a queue of any size, use two stacks or a head index:

js
class Queue {
  #items = [];
  #head = 0;
  enqueue(x) { this.#items.push(x); }
  dequeue() {
    if (this.#head >= this.#items.length) return undefined;
    const x = this.#items[this.#head];
    // release the reference
    this.#items[this.#head++] = undefined;
    if (this.#head > 32 && this.#head * 2 > this.#items.length) {
      // compact
      this.#items = this.#items.slice(this.#head);
      this.#head = 0;
    }
    return x;
  }
}

The = undefined matters: without it the array keeps a reference to every dequeued item and the queue leaks. Compacting occasionally keeps the backing array from growing without bound.

Choosing, quickly

Need Use
Fixed known shape, serialised object
Dynamic keys, non-string keys, ordering Map
Membership, dedupe Set
Metadata keyed by an object you don’t own WeakMap
LIFO array push/pop
FIFO array with a head index, not shift
Untrusted keys Map or Object.create(null)

Interview angle 6

  • “Map or object?” - Map for dynamic keys, non-string keys, frequent insertion and deletion, and guaranteed insertion order. Object for a known shape you will serialise. Objects coerce keys to strings and sort integer-like keys first, which surprises people relying on insertion order.
  • “Why would you use Object.create(null)?” - it has no prototype, so a user-supplied key like constructor or __proto__ is not already present and cannot pollute anything. A Map gives the same safety, and both are the answer for untrusted keys.
  • “What is a WeakMap for?” - attaching data to an object whose lifetime you do not control, such as a DOM node. Keys are held weakly, so the entry disappears when the key is collected. A normal Map would keep every key alive forever.
  • “Why can’t you iterate a WeakMap?” - because its contents can change whenever the garbage collector runs, so iteration order and size are not well defined. The lack of enumeration is what makes the weakness possible, not an oversight.
  • “How do you implement a queue in JavaScript?” - not with shift(), which is O(n) because it reindexes the whole array. Keep a head index and advance it, null out the slot you consumed so it can be collected, and compact occasionally.
  • “Why is new Set([NaN, NaN]).size equal to 1?” - Map and Set key by SameValueZero, under which NaN equals itself. That is also why Set.has(NaN) works where Array.indexOf(NaN) returns -1.