JavaScript Interview Cheat Sheet — Map, Set, Array, and the Gotchas That Bite Candidates

The JavaScript idioms, built-in Map/Set/Array APIs, and language gotchas that show up in coding interview solutions — Array.shift's accidental O(n²), the missing heap and deque, and Object[k] polymorphic-cache traps.

Bookmark this page. This is the JavaScript equivalent of the Python interview cheat sheet — the built-in Map / Set / Array APIs, the idioms that read as interview-idiomatic, and the language gotchas that turn correct algorithms into TLE submissions (Array.shift is O(n), there's no built-in heap or deque). Complements the Big-O cheat sheet; this one is about how to write it in JavaScript, not how fast it runs.

What JavaScript doesn't ship

Before the reference, the honest disclaimer. Compared to Python or Java, JavaScript's standard library is missing:

  • A binary heap / priority queue. Roll your own or import one. Fifteen-line class if you have to.
  • A proper deque. Array.push/pop are O(1), but Array.shift/unshift are O(n). For BFS, use a head-index counter or a small class.
  • A sorted container (TreeMap / TreeSet equivalent). No built-in balanced BST. In LeetCode contexts you can import sorted-btree or a similar package, but most solutions just brute-force with a sort per query.
  • bigint in most math routines. BigInt exists but doesn't compose with Math.*. For factorials, use BigInt with manual multiplication.

For most interview problems these gaps are surmountable. The ones that bite are (1) forgetting the deque gap and reaching for .shift(), and (2) implementing a bad heap in a hurry.

Data structures at a glance

StructureTypeOrdered?Notes
ArrayObject with numeric keysInsertionDynamic array. Access O(1); .shift O(n).
Object (as map)Prototype-basedInsertion (mostly)Polymorphic-cache regressions on hot paths.
MapBuilt-inInsertionPrefer for hot code. Keys can be any type.
SetBuilt-inInsertionO(1) has, add, delete.
WeakMap / WeakSetBuilt-inInsertionKeys must be objects. Rarely used in interviews.
StringImmutableCharacter orderO(n) concat in loop → use array join.

Map — the interview power tool

Map is what you'd reach for in Python as a dict, or Java as HashMap.

const map = new Map();
map.set('a', 1);              // O(1)
map.get('a');                 // 1                — O(1)
map.get('z');                 // undefined         — O(1)
map.has('a');                 // true              — O(1)
map.delete('a');              // O(1)
map.size;                     // property, not method

// Iteration — insertion order
for (const [k, v] of map) { ... }
for (const k of map.keys()) { ... }
for (const v of map.values()) { ... }

// From key-value pairs
const m = new Map([['a', 1], ['b', 2]]);

Prefer Map over Object for hot paths — Object property access is subject to V8's polymorphic-inline-cache regressions when the shape of the object mutates over time. Map is spec'd as sublinear and has consistent performance.

Set — dedup and membership

const set = new Set();
set.add(1);                   // O(1)
set.has(1);                   // O(1)
set.delete(1);                // O(1)
set.size;                     // property

// From an array — dedup in one line
const unique = [...new Set(nums)];

// Iteration — insertion order
for (const x of set) { ... }

No union/intersection built-ins. Roll them:

const intersect = new Set([...a].filter(x => b.has(x)));
const union = new Set([...a, ...b]);
const difference = new Set([...a].filter(x => !b.has(x)));

Array — the workhorse

const nums = [1, 2, 3];

nums[i];                      // O(1)
nums[i] = 42;                 // O(1)
nums.length;                  // O(1) property
nums.push(x);                 // O(1)*
nums.pop();                   // O(1)
nums.shift();                 // O(n) — every element moves. AVOID in loops.
nums.unshift(x);              // O(n)
nums.slice(a, b);             // O(k) — creates new array
nums.splice(i, delCount, ...items);  // O(n) — mutates
nums.includes(x);             // O(n)
nums.indexOf(x);              // O(n)
nums.find(pred);              // O(n)
nums.findIndex(pred);         // O(n)
nums.filter(pred);            // O(n) — new array
nums.map(fn);                 // O(n) — new array
nums.reduce(fn, init);        // O(n)
nums.sort();                  // O(n log n) — Timsort in V8, stable per ES2019 spec
nums.reverse();               // O(n) — in place
nums.join(sep);               // O(n)

// Sort with comparator — DEFAULT is string comparison!
nums.sort((a, b) => a - b);   // numeric ascending
nums.sort((a, b) => b - a);   // numeric descending

The default sort() compares as strings. [10, 2, 1].sort() returns [1, 10, 2] — lexicographic order. Always pass a comparator for numeric sorting.

Never use shift/unshift in a hot loop. They're O(n) and turn what should be O(n) algorithms into O(n²). For a queue, use a head-index counter:

// Queue emulation without shift
const queue = [];
let head = 0;
queue.push(x);                // enqueue — O(1)
const item = queue[head++];   // dequeue — O(1)
// Periodically compact: queue = queue.slice(head); head = 0;

Rolling your own heap

There's no built-in. This is the fifteen-line min-heap you'll find yourself pasting:

class MinHeap {
    constructor(compare = (a, b) => a - b) {
        this.data = [];
        this.compare = compare;
    }
    size() { return this.data.length; }
    peek() { return this.data[0]; }
    push(x) {
        this.data.push(x);
        this._siftUp(this.data.length - 1);
    }
    pop() {
        const top = this.data[0];
        const last = this.data.pop();
        if (this.data.length) {
            this.data[0] = last;
            this._siftDown(0);
        }
        return top;
    }
    _siftUp(i) {
        while (i > 0) {
            const parent = (i - 1) >> 1;
            if (this.compare(this.data[i], this.data[parent]) >= 0) break;
            [this.data[i], this.data[parent]] = [this.data[parent], this.data[i]];
            i = parent;
        }
    }
    _siftDown(i) {
        const n = this.data.length;
        while (true) {
            const l = 2 * i + 1, r = 2 * i + 2;
            let smallest = i;
            if (l < n && this.compare(this.data[l], this.data[smallest]) < 0) smallest = l;
            if (r < n && this.compare(this.data[r], this.data[smallest]) < 0) smallest = r;
            if (smallest === i) break;
            [this.data[i], this.data[smallest]] = [this.data[smallest], this.data[i]];
            i = smallest;
        }
    }
}

// Usage
const heap = new MinHeap();
heap.push(3); heap.push(1); heap.push(2);
heap.pop();                   // 1

For a max-heap, pass (a, b) => b - a as the comparator.

String operations

const s = 'Hello, World!';

s.length;                     // property
s[i];                         // O(1)
s.charAt(i);                  // O(1)  — same
s.charCodeAt(i);              // ASCII code
String.fromCharCode(97);      // 'a'
s.slice(a, b);                // O(k) — creates new string
s.substring(a, b);            // like slice but weird with negatives; prefer slice
s.split(',');                 // O(n)
s.trim(), s.trimStart(), s.trimEnd();
s.toLowerCase(), s.toUpperCase();
s.replace(/foo/g, 'bar');     // O(n) with regex
s.startsWith('H');            // O(k)
s.endsWith('!');              // O(k)
s.indexOf('W');               // O(n · m)
s.includes('W');              // same
'abc'.repeat(3);              // 'abcabcabc' — O(n · k)

The building rule. + on strings inside a loop is safer in modern V8 (rope structures) than in Python or Java — but relying on this optimisation is fragile. The safe habit:

const parts = [];
for (const chunk of chunks) parts.push(chunk);
const result = parts.join('');

Idioms you'll actually use

Destructuring

const [a, b] = [1, 2];
[a, b] = [b, a];                          // swap
const [head, ...tail] = [1, 2, 3, 4];     // head=1, tail=[2,3,4]
const { name, age } = person;
const { name: n, age: a } = person;       // rename

Spread

const merged = [...arr1, ...arr2];
const cloned = [...arr];
const objMerged = { ...obj1, ...obj2 };
Math.max(...nums);                        // spread into args

Arrow functions and implicit return

nums.map(x => x * 2);
nums.filter(x => x > 0);
nums.sort((a, b) => a - b);
nums.reduce((acc, x) => acc + x, 0);

Optional chaining and nullish coalescing

const value = obj?.nested?.field ?? 'default';

For-of vs for-in

for (const x of arr) { ... }     // values — use for arrays
for (const k in obj) { ... }     // keys — use for objects, but Map is better

Never for-in an array. It iterates enumerable properties including inherited ones; you'll get unexpected keys.

Common interview patterns

Frequency count

const counts = new Map();
for (const x of arr) counts.set(x, (counts.get(x) ?? 0) + 1);

Group by key

const groups = new Map();
for (const item of items) {
    const key = keyOf(item);
    if (!groups.has(key)) groups.set(key, []);
    groups.get(key).push(item);
}

Two-sum with hash map

function twoSum(nums, target) {
    const seen = new Map();
    for (let i = 0; i < nums.length; i++) {
        const complement = target - nums[i];
        if (seen.has(complement)) return [seen.get(complement), i];
        seen.set(nums[i], i);
    }
}

BFS with head-index queue (no deque)

function bfs(start, neighbours) {
    const visited = new Set([start]);
    const queue = [start];
    let head = 0;
    while (head < queue.length) {
        const node = queue[head++];
        for (const next of neighbours(node)) {
            if (!visited.has(next)) {
                visited.add(next);
                queue.push(next);
            }
        }
    }
}

Head-index-only saves the O(n) .shift() cost. Memory grows unboundedly across a long BFS; for very long runs, periodically queue = queue.slice(head); head = 0;.

Grid neighbours

const DIRS = [[0, 1], [0, -1], [1, 0], [-1, 0]];

for (const [dr, dc] of DIRS) {
    const nr = r + dr, nc = c + dc;
    if (nr >= 0 && nr < rows && nc >= 0 && nc < cols) {
        ...
    }
}

Gotchas — the traps

Array.sort default is lexicographic

[10, 2, 1].sort();                    // [1, 10, 2] — DON'T
[10, 2, 1].sort((a, b) => a - b);     // [1, 2, 10] — DO

Floating point on integer division

7 / 2;                        // 3.5
Math.floor(7 / 2);            // 3
(7 / 2) | 0;                  // 3  — bitwise-or with 0, integer coercion
Math.trunc(-7 / 2);           // -3 — truncation toward zero
Math.floor(-7 / 2);           // -4 — floor rounds toward -infinity

For integer division on positive numbers, (a / b) | 0 is idiomatic and fast. For negatives, be explicit — the two rounding modes disagree.

NaN !== NaN

NaN === NaN;                  // false
Number.isNaN(x);              // canonical NaN check

typeof null === 'object'

typeof null;                  // 'object' (JavaScript quirk since 1995)
typeof undefined;             // 'undefined'

Guard with if (x != null) (loose equality catches both null and undefined) or if (x !== null && x !== undefined).

Array(n).fill(0) vs shared reference

Array(3).fill([]);            // three references to the SAME array
Array.from({length: 3}, () => []);   // three DISTINCT arrays

Same issue as Python's [[0] * cols] * rows — always use Array.from({length: n}, () => ...) for arrays of mutable objects.

Map vs Object for keys that aren't strings

Object coerces keys to strings. map.set(1, 'a') and map.set('1', 'a') are two entries in a Map, but obj[1] and obj['1'] are the same entry in an Object. Use Map when integer keys matter.

for-in iterates inherited properties

Avoid for-in on arrays. On objects, use Object.keys(obj) or Object.entries(obj) for a clean iteration.

Truthiness of 0, '', and []

if (0) { }             // never runs
if ('') { }            // never runs
if ([]) { }            // ALWAYS runs — empty array is truthy
if ({}) { }            // ALWAYS runs — empty object is truthy

For array-empty checks, use if (arr.length === 0). For null-safety, use if (arr?.length).

parseInt needs a radix

parseInt('10');               // 10
parseInt('0x10');             // 16 — auto-detects hex
parseInt('10', 10);           // 10 — explicit and safe

Always pass the radix explicitly. Some legacy inputs get treated as hex or octal.

Complexity quick reference

Same as every other language for hash operations, with the JavaScript-specific "beware Array.shift" gotcha noted throughout. See the Big-O cheat sheet and the time-complexity-of-common-operations post for the exhaustive per-language table.

The patterns this cheat sheet backs

Every pattern post's JavaScript translation cashes in on the primitives above:

  • Sliding windowMap for the window count. Never Object.
  • Two-pointernums.sort((a, b) => a - b) — the comparator is mandatory for numeric sort.
  • Binary search — plain while loop; the boundary template ports one-to-one.
  • Monotonic stackArray with push / pop. Cheapest stack in the language.
  • Backtrackingcurrent.push(x); ...; current.pop(). Snapshot with [...current].
  • BFS vs DFS — head-index queue for BFS (see the pattern above). Never .shift().
  • Union-findInt32Array(n) for parent and rank, or plain Array(n).fill(0).map((_, i) => i) for parent.
  • Topological sortMap<number, number[]> for adjacency, head-index queue for Kahn's.

The idioms above aren't decoration. They're what makes JavaScript solutions read as interview-idiomatic and run in the time bound they claim.

Algotrek

A tutor for every algorithm pattern. Visual walkthroughs, smart hints, feedback that names your mistake.

Sign up

The path

Adaptive. Teaching-first. Built for engineers who'd rather understand than grind.

© 2026 Algotrek. All rights reserved. Live