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/popare O(1), butArray.shift/unshiftare 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-btreeor a similar package, but most solutions just brute-force with a sort per query. bigintin most math routines. BigInt exists but doesn't compose withMath.*. 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
| Structure | Type | Ordered? | Notes |
|---|---|---|---|
Array | Object with numeric keys | Insertion | Dynamic array. Access O(1); .shift O(n). |
Object (as map) | Prototype-based | Insertion (mostly) | Polymorphic-cache regressions on hot paths. |
Map | Built-in | Insertion | Prefer for hot code. Keys can be any type. |
Set | Built-in | Insertion | O(1) has, add, delete. |
WeakMap / WeakSet | Built-in | Insertion | Keys must be objects. Rarely used in interviews. |
String | Immutable | Character order | O(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 window —
Mapfor the window count. NeverObject. - Two-pointer —
nums.sort((a, b) => a - b)— the comparator is mandatory for numeric sort. - Binary search — plain
whileloop; the boundary template ports one-to-one. - Monotonic stack —
Arraywithpush/pop. Cheapest stack in the language. - Backtracking —
current.push(x); ...; current.pop(). Snapshot with[...current]. - BFS vs DFS — head-index queue for BFS (see the pattern above). Never
.shift(). - Union-find —
Int32Array(n)forparentandrank, or plainArray(n).fill(0).map((_, i) => i)forparent. - Topological sort —
Map<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.