code.davidloor.com

javascript · language · basics · 18 min read

JavaScript Foundations

Modern JavaScript for interview problems — let/const, Map and Set over plain objects, the sort comparator trap, and the array methods that solve most things.

JavaScript Foundations

Modern JavaScript (ES2015 and later) has a small, opinionated subset that handles every problem in the set. The harder thing about JS for interviews isn't the language — it's avoiding the rough edges (var, ==, Object as a hash map, the default sort). This guide is the floor.

Declarations: const, let, never var

const x = 1;        // can't be reassigned
let y = 2;          // reassignable
y = 3;
var z = 4;          // legacy. Don't use.

const for everything that doesn't reassign — most things, in practice. let for loop counters and accumulators that change. var has function-scoped hoisting that bites you in unexpected ways and has no upside in modern code.

Equality: ===, never ==

1 === 1            // true
1 === "1"          // false   — same value, different type
1 ==  "1"          // true    — type coercion. Sometimes useful. Never necessary.
null === undefined // false
null ==  undefined // true

Use === everywhere. The one exception you'll see is x == null to test for "null or undefined together" — but x === null || x === undefined is clearer.

Arrays: the workhorse

const xs = [1, 2, 3];
xs.push(4);                // → [1, 2, 3, 4]   end ops are amortized O(1)
xs.pop();                  // returns 4
xs.unshift(0);             // → [0, 1, 2, 3]   start ops are O(n) — avoid in tight loops
xs.shift();                // returns 0

xs[0];                     // 1
xs.length;                 // 3
xs.includes(2);            // true   — O(n) scan
xs.indexOf(2);             // 1, or -1 if absent

Slicing — copy without mutation

xs.slice(1, 3);            // → [2, 3]   start inclusive, end exclusive
xs.slice();                // shallow copy of whole array
xs.slice(-2);              // last two elements

xs.splice(1, 2);           // MUTATES — removes 2 elements starting at index 1
                           // returns the removed slice

slice and splice look alike and behave nothing alike. slice is non-destructive (use it). splice mutates (use it deliberately).

The methods that solve most things

xs.map(v => v * 2);                       // → [2, 4, 6]
xs.filter(v => v > 1);                    // → [2, 3]
xs.reduce((acc, v) => acc + v, 0);        // → 6
xs.find(v => v > 1);                      // → 2 (first match) or undefined
xs.findIndex(v => v > 1);                 // → 1 or -1
xs.some(v => v < 0);                      // → false
xs.every(v => v > 0);                     // → true
xs.forEach((v, i) => console.log(i, v));  // no return — side effects only

All non-mutating except forEach. Chain freely: nums.filter(v => v > 0).map(v => v * 2).reduce(...).

The single biggest JavaScript interview trap: Array.sort

[10, 2, 1].sort();                    // → [1, 10, 2]   ← LEXICOGRAPHIC
[10, 2, 1].sort((a, b) => a - b);     // → [1, 2, 10]   ← numeric

Array.sort converts elements to strings before comparing unless you pass a comparator. This is from ECMAScript-262, not a bug. Always pass a comparator for numbers.

xs.sort((a, b) => a - b);              // ascending
xs.sort((a, b) => b - a);              // descending
words.sort((a, b) => a.length - b.length);     // by length
points.sort((a, b) => a[0] - b[0] || a[1] - b[1]); // by x, then y

Knowing this saves you from at least one wrong answer in a real interview.

Map and Set — use them, not {} and []

Plain objects make terrible hash maps. They coerce keys to strings ({}[1] and {}["1"] are the same slot), inherit prototype properties (__proto__ is a real footgun), and don't track size.

const m = new Map();
m.set("k", 1);
m.set(42, "answer");
m.set(someObject, "value");      // any value as a key

m.get("k");                       // 1   (undefined if missing)
m.has("k");                       // true
m.delete("k");
m.size;                           // 1

for (const [k, v] of m) ...      // insertion order, guaranteed
const s = new Set([1, 2, 3]);
s.add(4);
s.delete(1);
s.has(2);                         // O(1) average
s.size;
[...s];                           // → [2, 3, 4]   spread to array

Plain {} is fine for a known fixed-shape record. Map is for "hash table" — Two Sum, grouping, counting, frequency tables.

Strings

const s = "anagram";
s[0];                              // "a"
s.length;                          // 7
s.slice(1, 4);                     // "nag"
s.includes("nag");                 // true
s.indexOf("g");                    // 3
s.split("");                       // ["a","n","a","g","r","a","m"]
[..."abc"];                        // → ["a","b","c"]  spread also works
"abc".repeat(3);                   // "abcabcabc"

s.charCodeAt(0);                   // 97   character to int
String.fromCharCode(97);           // "a"  int to character

Strings are immutable. s[0] = "x" silently fails (or throws in strict mode). To "mutate" a string, build with a list and join:

const chars = [..."hello"];
chars[0] = "H";
chars.join("");                    // "Hello"

To reverse: [...s].reverse().join(""). There's no built-in s.reverse().

for...of, for...in, and friends

for (const v of nums)         ...   // ✔ values — use for arrays, sets, maps
for (const k of map.keys())   ...   // explicit keys
for (const [k, v] of map)     ...   // pairs
for (const [i, v] of nums.entries()) ...   // index + value (array entries returns this)

for (const k in obj)          ...   // KEYS of an OBJECT — and also inherited keys (yikes)
                                    // Use Object.keys/entries instead.

for (let i = 0; i < n; i++)   ...   // classic — when you need the index

Rule of thumb: for...of for arrays/Sets/Maps, for(let i; ...) when the index matters, and Object.entries(obj) when iterating plain objects.

Destructuring and spread (every solution uses these)

const [a, b, c] = [1, 2, 3];           // array destructuring
const { val, next } = node;            // object destructuring
const { val: nodeVal } = node;         // rename
const [head, ...tail] = nums;          // rest

const merged = [...arr1, ...arr2];
const copied = [...arr1];               // shallow copy
const max = Math.max(...nums);          // spread args

const updated = { ...obj, count: 5 };  // copy + override one field

function f(...args) { ... }            // accept variable args

Functions: arrow vs regular

const add = (a, b) => a + b;            // implicit return
const square = x => x * x;              // single-arg, no parens needed
const fn = () => { /* multi-line */ return 1; };

function regular(a, b) { return a + b; } // works the same, except for `this`

Use arrows for callbacks (map, filter, comparators). They don't have their own this, which is exactly what you want inside .map(v => ...). The only time you want a regular function is when you specifically need a fresh this binding (rare in interview problems).

Optional and nullish

node?.next?.val                       // optional chaining — short-circuits on null/undefined
x ?? defaultValue                     // nullish coalescing — only null/undefined trigger
x || defaultValue                     // truthy-or — 0 and "" also trigger

const arr = list ?? [];               // common pattern when list might be null

Math you'll actually use

Math.max(a, b); Math.min(a, b);
Math.max(...nums);                    // spread an array
Math.floor(n / 2); Math.ceil(n / 2);
Math.abs(-5);                          // 5

(lo + hi) >> 1                         // integer midpoint, faster than Math.floor
                                       // works for non-negative ints in our range

Number.MAX_SAFE_INTEGER                // 2^53 - 1 — beyond this, ints lose precision
Number.MIN_SAFE_INTEGER

Infinity; -Infinity;                   // useful as initial best-so-far

Number precision warning: all JS numbers are 64-bit floats. 0.1 + 0.2 !== 0.3. For interview-sized integer arithmetic you're fine; for problems involving very large integers or precise decimals, use BigInt (10n ** 18n syntax) — but no v1 problem here needs that.

Classes (when you need them)

class Node {
  constructor(val, next = null) {
    this.val = val;
    this.next = next;
  }
}

const head = new Node(1, new Node(2));

Linked lists and trees in this site's harness use ListNode and TreeNode classes — your solution can just write new ListNode(v) directly; the harness injects the class globally.

Queue / deque — JavaScript has none

There's no built-in double-ended queue. Three options:

// 1. Just use an array. .shift() is O(n) but for problems with n ≤ 10^4
//    you usually don't notice. Simple and acceptable.
const q = [start];
while (q.length) {
  const node = q.shift();
  // ...
}

// 2. Index-based — never .shift(); track a head pointer.
const q = [start]; let head = 0;
while (head < q.length) {
  const node = q[head++];
  // ...
}
// Caveat: q never shrinks. Fine for one-pass BFS.

// 3. Two-stack trick — amortized O(1) on both ends.
class Queue {
  in = []; out = [];
  push(v)  { this.in.push(v); }
  shift()  {
    if (!this.out.length) while (this.in.length) this.out.push(this.in.pop());
    return this.out.pop();
  }
  get size() { return this.in.length + this.out.length; }
}

For the problems on this site, option 1 or 2 is fine.

The JavaScript you can skip (for this problem set)

Real JavaScript. Not needed for any v1 problem here.

Tour-of-the-problems map

Problem JavaScript tool that solves it
Two Sum Map from value → index
Valid Parentheses array as stack (push / pop)
Merge Two Sorted Lists dummy ListNode, three-pointer dance
Valid Anagram sort then join, or Map of char counts
Group Anagrams Map<string, string[]> keyed by sorted form
Maximum Subarray two running variables (Kadane)
Binary Search lo, hi, mid = (lo + hi) >> 1
Number of Islands Set of ${r},${c} strings; recursive DFS
3Sum [...nums].sort((a,b) => a-b) then two-pointer per i
Longest Substring No Repeat Map of char → last index; sliding window
Trapping Rain Water left/right max arrays, single pass
Word Break Set(words); 1D DP Array(n+1).fill(false)
Course Schedule adjacency Array.from({length:n},() => []) + queue for BFS topo
LCA of BST compare p and q to root.val; iterative descent

Once these tools feel automatic, the problems become exercises in which one to pick, not how to write code.