DEV Community

ZeeshanAli-0704
ZeeshanAli-0704

Posted on

Frontend JavaScript Utilities — From Scratch

Frontend JavaScript Utilities — From Scratch

A complete, interview-ready reference. Every utility is implemented from scratch, with:

  • Theory — what it does and why it matters
  • Implementation — heavily commented code
  • Complexity — time & space
  • Edge cases — the ones interviewers probe
  • Variations — the follow-ups you'll be asked

Interview mindset: don't just make it work. State assumptions, name edge cases out loud,
reason about time/space, and write tests. That is what separates a "hire" from a "no-hire".


Table of Contents


Level 1 — Language Fundamentals

Key this insight: array methods are called as arr.myMap(cb), so inside the method
this is the array. Also honor the optional second thisArg argument the natives accept.
Native methods skip holes in sparse arrays ([1, , 3]) — we can note that but a simple
index loop is acceptable in interviews.


1. Array.prototype.map

Theory: Returns a new array where each element is the result of callback(element, index, array).
It never mutates the original and always returns an array of the same length.

Array.prototype.myMap = function (callback, thisArg) {
  // Guard: callback must be callable.
  if (typeof callback !== "function") {
    throw new TypeError(callback + " is not a function");
  }

  const result = new Array(this.length); // preallocate for perf

  for (let i = 0; i < this.length; i++) {
    // `i in this` skips holes in sparse arrays, mirroring native behavior.
    if (i in this) {
      // thisArg lets callers control `this` inside the callback.
      result[i] = callback.call(thisArg, this[i], i, this);
    }
  }
  return result;
};

// Example
console.log([1, 2, 3].myMap((x) => x * 2)); // [2, 4, 6]
Enter fullscreen mode Exit fullscreen mode

Breaking down the pieces

What is this?
When you call a method with dot notation, this is the object to the left of the dot. Because
myMap is attached to Array.prototype, every array inherits it, so in [10, 20, 30].myMap(cb),
this is the array [10, 20, 30] — the same array native .map() would operate on.

What is this.length?
Since this is the array, this.length is just the array's length (3 above). It drives the
loop bounds and pre-sizes the result:

  • new Array(this.length) → pre-creates a same-size array so the engine avoids repeated resizing.
  • for (let i = 0; i < this.length; i++) → visits every index; this[i] reads each element.

What is thisArg?
thisArg is an optional second argument (the native map supports it too) that controls what
this refers to inside the callback. That's why we use callback.call(thisArg, ...) instead of
callback(...)Function.prototype.call runs the callback with its this set to thisArg.

const multiplier = {
  factor: 3,
  scale(x) {
    return x * this.factor; // `this` must be the multiplier object
  },
};

// Pass `multiplier` as thisArg so `this.factor` works inside scale().
console.log([1, 2, 3].myMap(multiplier.scale, multiplier)); // [3, 6, 9]
Enter fullscreen mode Exit fullscreen mode

thisArg only matters for regular functions. Arrow functions ignore it because they capture
this from where they were defined — so [1,2,3].myMap((x) => x * 2) never needs it.

What is i in this?
The if (i in this) check handles sparse arrays (arrays with holes). In [1, , 3], index 1
was never assigned, so 1 in arr is false. Native .map() skips holes rather than calling the
callback on them, and this check mirrors that. For dense (normal) arrays it's always true.

The three callback argumentscallback.call(thisArg, this[i], i, this) passes exactly what
native map gives you: the current element (this[i]), the index (i), and the whole
array (this). That's why arr.map((element, index, array) => ...) works.

Deep dive: callback.call(thisArg, this[i], i, this)

Every function has a built-in .call() method. It invokes the function immediately, but lets
you explicitly set (1) what this will be inside it (the first argument) and (2) the arguments
it receives (everything after the first, passed individually):

someFunction.call(whatThisShouldBe, arg1, arg2, arg3);
// ≡ someFunction(arg1, arg2, arg3) but with `this` forced to whatThisShouldBe
Enter fullscreen mode Exit fullscreen mode

Mapping that onto our line:

callback.call(thisArg, this[i], i, this);
//            └──┬───┘  └─┬──┘  │  └─┬─┘
//          this inside  elem  idx  whole
//          the callback              array
Enter fullscreen mode Exit fullscreen mode
Piece Role What it is
thisArg sets this in callback the optional 2nd arg the caller passed to myMap
this[i] 1st callback argument the current element (this is the array, i the index)
i 2nd callback argument the current index
this 3rd callback argument the whole array

Why .call() and not just callback(this[i], i, this)? The elements would still pass fine — but
you'd lose control of this inside the callback. .call(thisArg, ...) is exactly what makes the
optional thisArg feature work.

Related trio: .call(fn, a, b, c) passes args individually; .apply(fn, [a, b, c]) passes
them as an array; .bind(...) returns a new function instead of calling now. See #24–26.

Why pass the whole array as the 3rd argument?

The element and index tell the callback what and where. But some callbacks need to see other
elements
or the array as a whole to decide — the third argument gives them that access
without relying on an outside variable.

// Mark each element as a local peak (bigger than both neighbors).
[3, 7, 2, 8, 5].map((value, index, array) => {
  const left = array[index - 1] ?? -Infinity;   // needs the array!
  const right = array[index + 1] ?? -Infinity;  // needs the array!
  return value > left && value > right;
});
// [false, true, false, true, false]
Enter fullscreen mode Exit fullscreen mode

You can often close over the array variable instead (nums.map(v => v / nums.length)), but the
third argument matters when:

  1. The array has no namegetData().map((v, i, arr) => ...); arr is your only handle.
  2. The callback is reusable — a named function defined elsewhere can't close over a local array:
function isLocalMax(value, index, array) {
  return value > (array[index - 1] ?? -Infinity) &&
         value > (array[index + 1] ?? -Infinity);
}

[3, 7, 2].map(isLocalMax); // reused on any array — no closure needed
Enter fullscreen mode Exit fullscreen mode

Our myMap passes this as the third argument purely to mirror the native contract
(element, index, array). If it didn't, callbacks like isLocalMax would work with native .map()
but break under myMap — so it wouldn't be a faithful replacement. Most callbacks ignore this
argument (hence the common arr.map(x => ...)); it costs nothing and helps the minority that need it.

Arrow-function variation

You can write the whole thing as an arrow function — but note the catch: arrow functions have no
own this
, so we can't attach one to Array.prototype and rely on this being the array.
Instead, pass the array in explicitly:

// Arrow-function style: array is an explicit parameter, no `this` involved.
const myMap = (arr, callback, thisArg) => {
  if (typeof callback !== "function") {
    throw new TypeError(callback + " is not a function");
  }
  const result = new Array(arr.length);
  for (let i = 0; i < arr.length; i++) {
    if (i in arr) result[i] = callback.call(thisArg, arr[i], i, arr);
  }
  return result;
};

console.log(myMap([1, 2, 3], (x) => x * 2)); // [2, 4, 6]
Enter fullscreen mode Exit fullscreen mode
  • Why not Array.prototype.myMap = (callback) => {...}? An arrow function's this would be
    whatever surrounded its definition (often module.exports/window), not the array it's
    called on — so this.length would be wrong. Prototype methods must be regular functions.

  • Time: O(n) · Space: O(n)

  • Edge cases: empty array → []; callback not a function → throw; sparse arrays.

  • Variation: implement without preallocation using push.


2. Array.prototype.filter

Theory: Returns a new array containing only elements for which callback returns truthy.

Array.prototype.myFilter = function (callback, thisArg) {
  if (typeof callback !== "function") {
    throw new TypeError(callback + " is not a function");
  }

  const result = [];
  for (let i = 0; i < this.length; i++) {
    if (i in this && callback.call(thisArg, this[i], i, this)) {
      result.push(this[i]); // keep only truthy matches
    }
  }
  return result;
};

// Example
console.log([1, 2, 3, 4].myFilter((x) => x % 2 === 0)); // [2, 4]
Enter fullscreen mode Exit fullscreen mode
  • Time: O(n) · Space: O(k) where k = matches.
  • Edge case: predicate returning a truthy/falsy non-boolean (e.g. 0, "", NaN).

3. Array.prototype.reduce

Theory: Folds an array into a single accumulated value. If no initialValue is provided,
the first element becomes the accumulator and iteration starts at index 1.

Array.prototype.myReduce = function (callback, initialValue) {
  if (typeof callback !== "function") {
    throw new TypeError(callback + " is not a function");
  }

  let acc = initialValue;
  let startIndex = 0;
  const hasInitial = arguments.length >= 2; // distinguishes `undefined` from "missing"

  if (!hasInitial) {
    // Find first real element to seed the accumulator (skip holes).
    while (startIndex < this.length && !(startIndex in this)) startIndex++;
    if (startIndex >= this.length) {
      throw new TypeError("Reduce of empty array with no initial value");
    }
    acc = this[startIndex++];
  }

  for (let i = startIndex; i < this.length; i++) {
    if (i in this) acc = callback(acc, this[i], i, this);
  }
  return acc;
};

// Example
console.log([1, 2, 3, 4].myReduce((sum, x) => sum + x, 0)); // 10
console.log([1, 2, 3].myReduce((a, b) => a + b));           // 6 (no initial)
Enter fullscreen mode Exit fullscreen mode
  • Time: O(n) · Space: O(1)
  • Classic bug: using initialValue !== undefined instead of arguments.length >= 2. reduce(cb, undefined) MUST treat undefined as a provided seed.
  • Variation: myReduceRight iterates from the end.

arguments vs this (and why arguments.length >= 2)

this and arguments are two different automatic bindings that exist inside every regular
function — they answer different questions.

  • this = who the function was called on (the object left of the dot).
  • arguments = what arguments were actually passed (an array-like list, in order).
[1, 2, 3].myReduce(cb, 0);
// this      → [1, 2, 3]   (the receiver / array)
// arguments → [cb, 0]     arguments.length === 2
Enter fullscreen mode Exit fullscreen mode
this arguments
Answers who it's called on what was passed in
In the call above [1,2,3] (the array) [cb, 0] (the params)
Type the receiver object array-like list of args

Why arguments.length >= 2 matters. reduce must distinguish these two calls:

arr.myReduce(cb);            // NO initial value → seed with first element
arr.myReduce(cb, undefined); // initial value IS undefined → seed with undefined
Enter fullscreen mode Exit fullscreen mode

Checking initialValue !== undefined can't tell them apart — a missing 2nd parameter is also
undefined. But arguments.length counts how many arguments were actually passed, regardless
of their values:

arr.myReduce(cb);            // arguments.length === 1  → no initial
arr.myReduce(cb, undefined); // arguments.length === 2  → initial IS undefined
Enter fullscreen mode Exit fullscreen mode
// Proof:
[].myReduce((a, b) => a + b);             // throws "Reduce of empty array..." (no seed)
[].myReduce((a, b) => a + b, 0);          // 0 (seed provided)
[5].myReduce((a, b) => a + b, undefined); // undefined (undefined WAS the seed)
Enter fullscreen mode Exit fullscreen mode

Gotcha: arguments only exists in regular functions — arrow functions have no own
arguments (or this). In an arrow you'd use rest params instead: (...args) => args.length.


4. Array.prototype.find

Theory: Returns the first element for which the predicate is truthy, else undefined.
(findIndex returns the index or -1.)

Array.prototype.myFind = function (callback, thisArg) {
  if (typeof callback !== "function") throw new TypeError("callback is not a function");
  for (let i = 0; i < this.length; i++) {
    // Note: find does NOT skip holes; it visits every index (unlike map/filter).
    if (callback.call(thisArg, this[i], i, this)) return this[i];
  }
  return undefined;
};

console.log([5, 12, 8, 130].myFind((x) => x > 10)); // 12
Enter fullscreen mode Exit fullscreen mode
  • Time: O(n) worst case, early exit on match · Space: O(1)

5. Array.prototype.some

Theory: Returns true if at least one element passes the predicate. Short-circuits.
Empty array → false.

Array.prototype.mySome = function (callback, thisArg) {
  if (typeof callback !== "function") throw new TypeError("callback is not a function");
  for (let i = 0; i < this.length; i++) {
    if (i in this && callback.call(thisArg, this[i], i, this)) return true;
  }
  return false;
};

console.log([1, 2, 3].mySome((x) => x > 2)); // true
Enter fullscreen mode Exit fullscreen mode
  • Time: O(n), early exit · Space: O(1)

6. Array.prototype.every

Theory: Returns true if all elements pass. Short-circuits on first failure.
Empty array → true (vacuous truth).

Array.prototype.myEvery = function (callback, thisArg) {
  if (typeof callback !== "function") throw new TypeError("callback is not a function");
  for (let i = 0; i < this.length; i++) {
    if (i in this && !callback.call(thisArg, this[i], i, this)) return false;
  }
  return true;
};

console.log([2, 4, 6].myEvery((x) => x % 2 === 0)); // true
Enter fullscreen mode Exit fullscreen mode
  • Note: some and every are duals: every(p) === !some(!p).

7. Array.prototype.flat(depth)

Theory: Flattens nested arrays up to depth levels (default 1). Infinity fully flattens.

// Recursive version — clean and readable.
Array.prototype.myFlat = function (depth = 1) {
  const result = [];
  for (const item of this) {
    if (Array.isArray(item) && depth > 0) {
      // Spread the recursively-flattened sub-array one level shallower.
      result.push(...item.myFlat(depth - 1));
    } else {
      result.push(item);
    }
  }
  return result;
};

console.log([1, [2, [3, [4]]]].myFlat(Infinity)); // [1, 2, 3, 4]
console.log([1, [2, [3]]].myFlat(1));             // [1, 2, [3]]
Enter fullscreen mode Exit fullscreen mode

Iterative version (no recursion — common follow-up):

function flattenIterative(arr) {
  const stack = [...arr];
  const result = [];
  while (stack.length) {
    const next = stack.pop();
    if (Array.isArray(next)) stack.push(...next); // push children back
    else result.push(next);
  }
  return result.reverse(); // pop reverses order, so restore it
}
Enter fullscreen mode Exit fullscreen mode
  • Time: O(n) total elements · Space: O(n)
  • Follow-up: "flatten without recursion" → use the explicit stack version.

8. Array.prototype.flatMap

Theory: map then flat(1). Useful for expanding each element into 0..n items.

Array.prototype.myFlatMap = function (callback, thisArg) {
  return this.myMap((v, i, arr) => callback.call(thisArg, v, i, arr)).myFlat(1);
};

console.log([1, 2, 3].myFlatMap((x) => [x, x * 2])); // [1, 2, 2, 4, 3, 6]
Enter fullscreen mode Exit fullscreen mode
  • Only flattens one level — a frequent gotcha.

9. Array.prototype.includes

Theory: Returns true if the array contains value, using SameValueZero equality
(so NaN matches NaN, unlike indexOf).

Array.prototype.myIncludes = function (value, fromIndex = 0) {
  const len = this.length;
  // Handle negative fromIndex (counts from the end).
  let start = fromIndex < 0 ? Math.max(len + fromIndex, 0) : fromIndex;

  for (let i = start; i < len; i++) {
    const el = this[i];
    // SameValueZero: equal, OR both NaN.
    if (el === value || (el !== el && value !== value)) return true;
  }
  return false;
};

console.log([1, NaN, 3].myIncludes(NaN)); // true  (indexOf would fail here)
Enter fullscreen mode Exit fullscreen mode
  • Key difference vs indexOf: NaN handling via the el !== el trick.

10. Deep Clone (Objects, Arrays, Date, RegExp, circular refs)

Theory: Recursively copy so that mutating the clone never affects the original. We'll build it
up in 4 steps, each fixing a weakness of the previous one.

Step 1 — the naive one-liner (know its limits)

const clone = JSON.parse(JSON.stringify(obj));
Enter fullscreen mode Exit fullscreen mode

Works for plain JSON data, but breaks on: Date (becomes a string), RegExp/Map/Set
(become {}), undefined/functions (dropped), and throws on circular references. Interviewers
mention this only to then ask you to do better.

Step 2 — recursion for objects & arrays

Handle the core case: recurse into every key, preserving array-vs-object shape.

function deepClone(value) {
  // Primitives (string, number, boolean, null, undefined) copy by value already.
  if (value === null || typeof value !== "object") return value;

  // Preserve the shape: array stays array, object stays object.
  const clone = Array.isArray(value) ? [] : {};

  for (const key of Object.keys(value)) {
    clone[key] = deepClone(value[key]); // recurse into each property
  }
  return clone;
}

const a = { n: 1, nested: { m: 2 }, list: [3, 4] };
const b = deepClone(a);
b.nested.m = 99;
console.log(a.nested.m); // 2 — original untouched
Enter fullscreen mode Exit fullscreen mode

Still missing: special built-ins (Date, RegExp…) and cycles (would infinite-loop).

Step 3 — add special built-in types

Each built-in needs its own copy logic, checked before the generic object branch.

function deepClone(value) {
  if (value === null || typeof value !== "object") return value;

  // --- special built-ins ---
  if (value instanceof Date) return new Date(value.getTime());
  if (value instanceof RegExp) return new RegExp(value.source, value.flags);
  if (value instanceof Map) {
    const out = new Map();
    value.forEach((v, k) => out.set(deepClone(k), deepClone(v)));
    return out;
  }
  if (value instanceof Set) {
    const out = new Set();
    value.forEach((v) => out.add(deepClone(v)));
    return out;
  }

  // --- generic object / array ---
  const clone = Array.isArray(value) ? [] : {};
  for (const key of Object.keys(value)) {
    clone[key] = deepClone(value[key]);
  }
  return clone;
}
Enter fullscreen mode Exit fullscreen mode

Still missing: circular references (a.self = a → infinite recursion → stack overflow).

Step 4 — handle cycles with a WeakMap (the full answer)

A WeakMap remembers "original → its clone". Before recursing into an object, register its clone
first
; if we meet the same object again, return the clone we already started instead of looping.

function deepClone(value, seen = new WeakMap()) {
  // Primitives (and functions) are returned as-is.
  if (value === null || typeof value !== "object") return value;

  // Special built-in objects.
  if (value instanceof Date) return new Date(value.getTime());
  if (value instanceof RegExp) return new RegExp(value.source, value.flags);
  if (value instanceof Map) {
    const out = new Map();
    seen.set(value, out);
    value.forEach((v, k) => out.set(deepClone(k, seen), deepClone(v, seen)));
    return out;
  }
  if (value instanceof Set) {
    const out = new Set();
    seen.set(value, out);
    value.forEach((v) => out.add(deepClone(v, seen)));
    return out;
  }

  // Circular reference: return the copy we already started.
  if (seen.has(value)) return seen.get(value);

  // Preserve array vs plain object shape.
  const clone = Array.isArray(value) ? [] : {};
  seen.set(value, clone); // register BEFORE recursing to break cycles

  // Reflect.ownKeys also copies Symbol keys (Object.keys skips them).
  for (const key of Reflect.ownKeys(value)) {
    clone[key] = deepClone(value[key], seen);
  }
  return clone;
}

// Example — circular reference
const a = { name: "x" };
a.self = a;
const b = deepClone(a);
console.log(b.self === b);  // true (cycle preserved, not infinite loop)
console.log(b !== a);       // true
Enter fullscreen mode Exit fullscreen mode
  • Time: O(n) nodes · Space: O(n)
  • What each step added: (1) baseline → (2) recursion → (3) built-ins → (4) cycles + symbol keys.
  • Why WeakMap? keys are held weakly, so cloned originals can still be garbage-collected.
  • Note: structuredClone() is the built-in modern answer, but interviewers want the manual one.

Level 2 — Objects

11. Object.assign

Theory: Copies own enumerable properties from sources into the target (shallow), mutating and returning the target. Triggers setters and copies symbols.

function objectAssign(target, ...sources) {
  if (target == null) throw new TypeError("Cannot convert null/undefined to object");
  const to = Object(target); // boxes primitives

  for (const source of sources) {
    if (source == null) continue; // null/undefined sources are skipped
    for (const key of Reflect.ownKeys(source)) {
      // Only own ENUMERABLE properties are copied.
      const desc = Object.getOwnPropertyDescriptor(source, key);
      if (desc && desc.enumerable) to[key] = source[key];
    }
  }
  return to;
}

console.log(objectAssign({ a: 1 }, { b: 2 }, { a: 9 })); // { a: 9, b: 2 }
Enter fullscreen mode Exit fullscreen mode
  • Shallow only — nested objects are shared by reference.

12. Object.entries

Theory: Returns an array of [key, value] pairs for own enumerable string-keyed props.

function objectEntries(obj) {
  if (obj == null) throw new TypeError("Cannot convert null/undefined to object");
  const result = [];
  for (const key of Object.keys(obj)) {
    result.push([key, obj[key]]);
  }
  return result;
}

console.log(objectEntries({ a: 1, b: 2 })); // [["a",1],["b",2]]
Enter fullscreen mode Exit fullscreen mode

13. Object.fromEntries

Theory: Inverse of entries — builds an object from [key, value] pairs. Accepts any iterable.

function objectFromEntries(iterable) {
  const obj = {};
  for (const [key, value] of iterable) {
    obj[key] = value;
  }
  return obj;
}

console.log(objectFromEntries([["a", 1], ["b", 2]])); // { a: 1, b: 2 }
// Bonus: works with a Map directly since Map is iterable of entries.
Enter fullscreen mode Exit fullscreen mode

14. Deep Freeze

Theory: Object.freeze is shallow. Deep freeze recurses so nested objects are also immutable.
Guard against cycles.

function deepFreeze(obj, seen = new WeakSet()) {
  if (obj === null || typeof obj !== "object" || seen.has(obj)) return obj;
  seen.add(obj);

  // Freeze children first, then the object itself.
  for (const key of Reflect.ownKeys(obj)) {
    deepFreeze(obj[key], seen);
  }
  return Object.freeze(obj);
}

const cfg = deepFreeze({ a: { b: 1 } });
cfg.a.b = 99; // silently ignored (throws in strict mode)
console.log(cfg.a.b); // 1
Enter fullscreen mode Exit fullscreen mode

15. Deep Merge

Theory: Recursively merge sources into a target. Plain objects merge key-by-key; arrays and
primitives typically overwrite (define your policy explicitly in the interview).

function isPlainObject(v) {
  return v !== null && typeof v === "object" && !Array.isArray(v);
}

function deepMerge(target, ...sources) {
  for (const source of sources) {
    if (!isPlainObject(source)) continue;
    for (const key of Object.keys(source)) {
      if (isPlainObject(source[key]) && isPlainObject(target[key])) {
        deepMerge(target[key], source[key]); // recurse into nested objects
      } else {
        target[key] = source[key]; // overwrite (arrays/primitives)
      }
    }
  }
  return target;
}

console.log(deepMerge({ a: { x: 1 } }, { a: { y: 2 } })); // { a: { x: 1, y: 2 } }
Enter fullscreen mode Exit fullscreen mode
  • Design decision to state: how do you merge arrays? (concat vs replace vs index-merge).

Level 3 — Function Utilities

These are the most common Google frontend warm-ups. Know them cold.

16. Debounce

Theory: Delays invoking fn until delay ms have passed without a new call.
Great for search-as-you-type, resize, autosave.

function debounce(fn, delay) {
  let timerId;
  function debounced(...args) {
    clearTimeout(timerId); // cancel the pending call
    timerId = setTimeout(() => fn.apply(this, args), delay); // preserve `this`
  }
  debounced.cancel = () => clearTimeout(timerId);
  return debounced;
}

const log = debounce(() => console.log("fired"), 300);
log(); log(); log(); // only the last one fires, 300ms after the final call
Enter fullscreen mode Exit fullscreen mode

Production version (leading, trailing, maxWait): see #82 in Level 13.

  • Edge cases: preserve this and args; provide cancel.

17. Throttle

Theory: Ensures fn runs at most once per interval. Good for scroll, mousemove.

function throttle(fn, interval) {
  let lastTime = 0;
  let timerId = null;

  return function throttled(...args) {
    const now = Date.now();
    const remaining = interval - (now - lastTime);

    if (remaining <= 0) {
      // Enough time passed — run immediately (leading edge).
      lastTime = now;
      fn.apply(this, args);
    } else if (!timerId) {
      // Schedule a trailing call for the leftover window.
      timerId = setTimeout(() => {
        lastTime = Date.now();
        timerId = null;
        fn.apply(this, args);
      }, remaining);
    }
  };
}
Enter fullscreen mode Exit fullscreen mode
  • Debounce vs throttle: debounce waits for silence; throttle guarantees a steady cadence.

18. once()

Theory: Wraps fn so it runs only the first time; later calls return the cached result.

function once(fn) {
  let called = false;
  let result;
  return function (...args) {
    if (!called) {
      called = true;
      result = fn.apply(this, args);
    }
    return result;
  };
}

const init = once(() => console.log("init") || 42);
init(); // "init" -> 42
init(); // 42 (no log)
Enter fullscreen mode Exit fullscreen mode

19. memoize()

Theory: Caches results by arguments so repeated calls with the same inputs are O(1).

function memoize(fn, resolver) {
  const cache = new Map();
  return function (...args) {
    // Default key = JSON of args. `resolver` lets callers customize the key.
    const key = resolver ? resolver(...args) : JSON.stringify(args);
    if (cache.has(key)) return cache.get(key);
    const result = fn.apply(this, args);
    cache.set(key, result);
    return result;
  };
}

const slowSquare = memoize((n) => n * n);
console.log(slowSquare(4)); // computes -> 16
console.log(slowSquare(4)); // cached  -> 16
Enter fullscreen mode Exit fullscreen mode
  • Caveat: JSON.stringify fails on functions/circular args → allow a custom resolver.

20. curry()

Theory: Transforms f(a, b, c) into f(a)(b)(c). Collect args until arity is met, then call.

function curry(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) {
      // Enough args collected — invoke the original.
      return fn.apply(this, args);
    }
    // Otherwise return a function that gathers more args.
    return (...next) => curried.apply(this, [...args, ...next]);
  };
}

const sum = curry((a, b, c) => a + b + c);
console.log(sum(1)(2)(3)); // 6
console.log(sum(1, 2)(3)); // 6
console.log(sum(1)(2, 3)); // 6
Enter fullscreen mode Exit fullscreen mode
  • Note: relies on fn.length (declared parameter count) — rest params break it.

21. compose()

Theory: Right-to-left function composition: compose(f, g)(x) === f(g(x)).

function compose(...fns) {
  return function (initial) {
    return fns.reduceRight((acc, fn) => fn(acc), initial);
  };
}

const add1 = (x) => x + 1;
const double = (x) => x * 2;
console.log(compose(add1, double)(5)); // add1(double(5)) = 11
Enter fullscreen mode Exit fullscreen mode

22. pipe()

Theory: Left-to-right composition (the reading order): pipe(f, g)(x) === g(f(x)).

function pipe(...fns) {
  return function (initial) {
    return fns.reduce((acc, fn) => fn(acc), initial);
  };
}

console.log(pipe(add1, double)(5)); // double(add1(5)) = 12
Enter fullscreen mode Exit fullscreen mode

23. partial()

Theory: Pre-fills leading arguments, returning a function that takes the rest.

function partial(fn, ...preset) {
  return function (...rest) {
    return fn.apply(this, [...preset, ...rest]);
  };
}

const greet = (greeting, name) => `${greeting}, ${name}!`;
const hello = partial(greet, "Hello");
console.log(hello("Ada")); // "Hello, Ada!"
Enter fullscreen mode Exit fullscreen mode
  • Variation: support placeholders (_) to skip positions.

24. Function.prototype.myBind

Theory: Returns a new function bound to a given this and optional preset args. Must also
support being used as a constructor with new (then this binding is ignored).

Function.prototype.myBind = function (thisArg, ...boundArgs) {
  const targetFn = this;
  if (typeof targetFn !== "function") throw new TypeError("Bind must be called on a function");

  function bound(...callArgs) {
    // If called with `new`, `this` is a fresh instance → ignore thisArg.
    const isNew = this instanceof bound;
    return targetFn.apply(isNew ? this : thisArg, [...boundArgs, ...callArgs]);
  }

  // Maintain the prototype chain for `new boundFn()`.
  if (targetFn.prototype) {
    bound.prototype = Object.create(targetFn.prototype);
  }
  return bound;
};

function greet(greeting, name) { return `${greeting}, ${name}, I am ${this.role}`; }
const bound = greet.myBind({ role: "dev" }, "Hi");
console.log(bound("Sam")); // "Hi, Sam, I am dev"
Enter fullscreen mode Exit fullscreen mode

25. Function.prototype.myCall

Theory: Invokes the function immediately with a given this and individual arguments.

Function.prototype.myCall = function (thisArg, ...args) {
  // Box primitives; default to globalThis when null/undefined.
  thisArg = thisArg == null ? globalThis : Object(thisArg);

  const fnKey = Symbol("fn"); // unique key avoids clobbering existing props
  thisArg[fnKey] = this;      // temporarily attach the function
  const result = thisArg[fnKey](...args); // `this` inside now = thisArg
  delete thisArg[fnKey];      // clean up
  return result;
};

function whoAmI() { return this.name; }
console.log(whoAmI.myCall({ name: "Neo" })); // "Neo"
Enter fullscreen mode Exit fullscreen mode

26. Function.prototype.myApply

Theory: Like call, but arguments are passed as a single array.

Function.prototype.myApply = function (thisArg, argsArray) {
  thisArg = thisArg == null ? globalThis : Object(thisArg);
  const args = argsArray || [];

  const fnKey = Symbol("fn");
  thisArg[fnKey] = this;
  const result = thisArg[fnKey](...args);
  delete thisArg[fnKey];
  return result;
};

function sum(a, b) { return a + b + this.base; }
console.log(sum.myApply({ base: 10 }, [1, 2])); // 13
Enter fullscreen mode Exit fullscreen mode
  • call vs apply: "call = Comma-separated, apply = Array" — handy mnemonic.

Level 4 — Promise Exercises

27. Promise.all

Theory: Resolves when all promises resolve, preserving input order. Rejects immediately if
any rejects (fail-fast).

function promiseAll(promises) {
  return new Promise((resolve, reject) => {
    const results = [];
    let completed = 0;
    const items = Array.from(promises);
    if (items.length === 0) return resolve([]); // empty → resolve immediately

    items.forEach((p, index) => {
      // Wrap non-promises so `.then` always works.
      Promise.resolve(p).then((value) => {
        results[index] = value;       // keep original order via index
        if (++completed === items.length) resolve(results);
      }, reject);                     // first rejection rejects the whole thing
    });
  });
}

promiseAll([Promise.resolve(1), Promise.resolve(2)]).then(console.log); // [1, 2]
Enter fullscreen mode Exit fullscreen mode
  • Edge cases: empty array → []; non-promise values are allowed.

28. Promise.allSettled

Theory: Waits for all to settle and never rejects. Each result is
{status:"fulfilled", value} or {status:"rejected", reason}.

function promiseAllSettled(promises) {
  return new Promise((resolve) => {
    const results = [];
    let completed = 0;
    const items = Array.from(promises);
    if (items.length === 0) return resolve([]);

    items.forEach((p, index) => {
      Promise.resolve(p).then(
        (value) => { results[index] = { status: "fulfilled", value }; },
        (reason) => { results[index] = { status: "rejected", reason }; }
      ).finally(() => {
        if (++completed === items.length) resolve(results);
      });
    });
  });
}
Enter fullscreen mode Exit fullscreen mode

29. Promise.any

Theory: Resolves with the first fulfilled value. Rejects only if all reject, with an
AggregateError collecting every reason.

function promiseAny(promises) {
  return new Promise((resolve, reject) => {
    const errors = [];
    let rejectedCount = 0;
    const items = Array.from(promises);
    if (items.length === 0) {
      return reject(new AggregateError([], "All promises were rejected"));
    }

    items.forEach((p, index) => {
      Promise.resolve(p).then(resolve, (err) => {
        errors[index] = err;
        if (++rejectedCount === items.length) {
          reject(new AggregateError(errors, "All promises were rejected"));
        }
      });
    });
  });
}
Enter fullscreen mode Exit fullscreen mode

30. Promise.race

Theory: Settles as soon as the first promise settles (fulfilled OR rejected).

function promiseRace(promises) {
  return new Promise((resolve, reject) => {
    for (const p of promises) {
      // Whichever settles first wins; the rest are ignored.
      Promise.resolve(p).then(resolve, reject);
    }
  });
}
Enter fullscreen mode Exit fullscreen mode
  • race vs any: race settles on first settle (incl. reject); any needs first fulfill.

31. Retry Promise

Theory: Retry an async operation up to retries times before giving up. Add backoff for
production (exponential backoff in Level 13).

function retry(fn, retries) {
  return new Promise((resolve, reject) => {
    function attempt(remaining) {
      fn()
        .then(resolve)
        .catch((err) => {
          if (remaining <= 0) reject(err);       // out of tries → propagate
          else attempt(remaining - 1);            // try again
        });
    }
    attempt(retries);
  });
}

let n = 0;
const flaky = () => (++n < 3 ? Promise.reject("fail") : Promise.resolve("ok"));
retry(flaky, 5).then(console.log); // "ok" on the 3rd try
Enter fullscreen mode Exit fullscreen mode

32. Timeout Promise

Theory: Reject if a promise doesn't settle within ms. Implemented by racing against a timer.

function timeout(promise, ms) {
  const timer = new Promise((_, reject) =>
    setTimeout(() => reject(new Error(`Timed out after ${ms}ms`)), ms)
  );
  return Promise.race([promise, timer]);
}

timeout(fetch("/data"), 5000).catch(console.error);
Enter fullscreen mode Exit fullscreen mode
  • Note: the underlying work still runs; timeout only stops waiting. Use AbortController to actually cancel a real fetch.

33. Promise Queue (Concurrency = 3)

Theory: Run async tasks with a cap on how many run simultaneously. Each task is a
function returning a promise.

class PromiseQueue {
  constructor(concurrency = 3) {
    this.concurrency = concurrency;
    this.running = 0;
    this.queue = [];
  }

  add(taskFn) {
    return new Promise((resolve, reject) => {
      // Store the task plus its outer resolve/reject so we can settle later.
      this.queue.push({ taskFn, resolve, reject });
      this._next();
    });
  }

  _next() {
    // Fill available slots up to the concurrency limit.
    while (this.running < this.concurrency && this.queue.length) {
      const { taskFn, resolve, reject } = this.queue.shift();
      this.running++;
      Promise.resolve(taskFn())
        .then(resolve, reject)
        .finally(() => {
          this.running--;
          this._next(); // a slot freed up → pull the next task
        });
    }
  }
}

const q = new PromiseQueue(3);
for (let i = 0; i < 10; i++) {
  q.add(() => new Promise((r) => setTimeout(() => r(i), 100)));
}
Enter fullscreen mode Exit fullscreen mode
  • Time to first task: O(1) · Steady-state parallelism = concurrency.

34. Task Scheduler (Max Parallel)

Theory: Same idea as the queue but exposed as a single runTask API — a common phrasing.

function createScheduler(maxParallel) {
  let active = 0;
  const waiting = [];

  function runTask(task) {
    return new Promise((resolve, reject) => {
      const run = () => {
        active++;
        Promise.resolve(task())
          .then(resolve, reject)
          .finally(() => {
            active--;
            if (waiting.length) waiting.shift()(); // wake next waiter
          });
      };
      if (active < maxParallel) run();
      else waiting.push(run); // park until a slot frees
    });
  }

  return { runTask };
}
Enter fullscreen mode Exit fullscreen mode

Level 5 — Async JavaScript

35. sleep(ms)

Theory: Promisified delay — the building block for pacing async loops.

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

async function demo() {
  console.log("start");
  await sleep(1000);
  console.log("1s later");
}
Enter fullscreen mode Exit fullscreen mode

36. asyncPool() (Max Concurrent Requests)

Theory: Map over items with an async iteratorFn, keeping at most poolLimit in flight,
and return results in input order.

async function asyncPool(poolLimit, items, iteratorFn) {
  const results = [];
  const executing = new Set();

  for (const item of items) {
    // Kick off the task and remember its promise.
    const p = Promise.resolve().then(() => iteratorFn(item));
    results.push(p);
    executing.add(p);

    // When it finishes, remove it from the in-flight set.
    p.finally(() => executing.delete(p));

    // If we're at the limit, wait for ANY task to finish before continuing.
    if (executing.size >= poolLimit) {
      await Promise.race(executing);
    }
  }
  return Promise.all(results); // preserves order
}

// asyncPool(2, [1,2,3,4], (n) => fetch(`/x/${n}`));
Enter fullscreen mode Exit fullscreen mode

37. Sequential Executor

Theory: Run tasks strictly one after another; each waits for the previous.

async function runSequential(tasks) {
  const results = [];
  for (const task of tasks) {
    results.push(await task()); // await forces order
  }
  return results;
}
Enter fullscreen mode Exit fullscreen mode
  • Reduce version: tasks.reduce((p, t) => p.then((acc) => t().then((r) => [...acc, r])), Promise.resolve([])).

38. Parallel Executor

Theory: Start everything at once and wait for all.

async function runParallel(tasks) {
  return Promise.all(tasks.map((task) => task()));
}
Enter fullscreen mode Exit fullscreen mode
  • Contrast: sequential = sum of durations; parallel = max duration.

39. Cancellation Token

Theory: A signal object callers can check/await to abort long work cooperatively.
Mirrors the standard AbortController.

function createCancelToken() {
  let cancelled = false;
  const listeners = [];
  return {
    cancel() {
      cancelled = true;
      listeners.forEach((fn) => fn());
    },
    get isCancelled() { return cancelled; },
    onCancel(fn) { listeners.push(fn); },
    throwIfCancelled() { if (cancelled) throw new Error("Cancelled"); },
  };
}

async function work(token) {
  for (let i = 0; i < 1000; i++) {
    token.throwIfCancelled(); // cooperative checkpoint
    await sleep(10);
  }
}
Enter fullscreen mode Exit fullscreen mode
  • Real-world: prefer the native AbortController / signal passed to fetch.

Level 6 — Event System

40. EventEmitter (on, off, once, emit)

Theory: Pub/sub keyed by event name. on subscribes, off unsubscribes, once auto-removes
after one fire, emit calls all listeners with the payload.

class EventEmitter {
  constructor() {
    this.events = new Map(); // eventName -> Set of listeners
  }

  on(event, listener) {
    if (!this.events.has(event)) this.events.set(event, new Set());
    this.events.get(event).add(listener);
    return this; // chainable
  }

  off(event, listener) {
    this.events.get(event)?.delete(listener);
    return this;
  }

  once(event, listener) {
    // Wrapper removes itself after the first call.
    const wrapper = (...args) => {
      this.off(event, wrapper);
      listener.apply(this, args);
    };
    wrapper.original = listener; // allow off(once) by original ref
    return this.on(event, wrapper);
  }

  emit(event, ...args) {
    const listeners = this.events.get(event);
    if (!listeners) return false;
    // Copy to a snapshot so once()-removals mid-emit don't skip listeners.
    [...listeners].forEach((fn) => fn.apply(this, args));
    return true;
  }
}

const bus = new EventEmitter();
bus.on("hi", (name) => console.log("hi " + name));
bus.emit("hi", "Ada"); // "hi Ada"
Enter fullscreen mode Exit fullscreen mode

41. Pub/Sub

Theory: Same pattern, framed as subscribe/publish, returning an unsubscribe handle —
the ergonomic modern API.

function createPubSub() {
  const topics = new Map();
  return {
    subscribe(topic, handler) {
      if (!topics.has(topic)) topics.set(topic, new Set());
      topics.get(topic).add(handler);
      return () => topics.get(topic).delete(handler); // unsubscribe fn
    },
    publish(topic, data) {
      topics.get(topic)?.forEach((h) => h(data));
    },
  };
}
Enter fullscreen mode Exit fullscreen mode

42. Observable (basic)

Theory: A push-based stream: an Observable accepts a subscriber {next, error, complete}
and returns an unsubscribe function. This is the essence of RxJS.

class Observable {
  constructor(subscribeFn) {
    this._subscribe = subscribeFn; // producer function
  }

  subscribe(observer) {
    // Normalize a bare next-callback into an observer object.
    const safe = typeof observer === "function" ? { next: observer } : observer;
    const unsub = this._subscribe({
      next: (v) => safe.next?.(v),
      error: (e) => safe.error?.(e),
      complete: () => safe.complete?.(),
    });
    return { unsubscribe: typeof unsub === "function" ? unsub : () => {} };
  }
}

// Example: emit 1,2,3 then complete.
const obs = new Observable((o) => {
  o.next(1); o.next(2); o.next(3); o.complete();
});
obs.subscribe({ next: console.log, complete: () => console.log("done") });
Enter fullscreen mode Exit fullscreen mode

Level 7 — Data Structures

43. Stack (LIFO)

class Stack {
  constructor() { this.items = []; }
  push(x) { this.items.push(x); }
  pop() { return this.items.pop(); }              // O(1)
  peek() { return this.items[this.items.length - 1]; }
  get size() { return this.items.length; }
  isEmpty() { return this.items.length === 0; }
}
Enter fullscreen mode Exit fullscreen mode
  • All ops O(1). Uses: undo, DFS, expression parsing, browser history.

44. Queue (FIFO)

Theory: Array shift() is O(n). For O(1) dequeue, use two indices (head/tail) or a linked list.

class Queue {
  constructor() { this.items = {}; this.head = 0; this.tail = 0; }
  enqueue(x) { this.items[this.tail++] = x; }     // O(1)
  dequeue() {
    if (this.isEmpty()) return undefined;
    const val = this.items[this.head];
    delete this.items[this.head++];
    return val;                                    // O(1)
  }
  peek() { return this.items[this.head]; }
  get size() { return this.tail - this.head; }
  isEmpty() { return this.size === 0; }
}
Enter fullscreen mode Exit fullscreen mode

45. Deque (Double-Ended Queue)

class Deque {
  constructor() { this.items = {}; this.head = 0; this.tail = 0; }
  pushBack(x)  { this.items[this.tail++] = x; }
  pushFront(x) { this.items[--this.head] = x; }
  popBack()  { if (this.isEmpty()) return; const v = this.items[--this.tail]; delete this.items[this.tail]; return v; }
  popFront() { if (this.isEmpty()) return; const v = this.items[this.head]; delete this.items[this.head++]; return v; }
  get size() { return this.tail - this.head; }
  isEmpty() { return this.size === 0; }
}
Enter fullscreen mode Exit fullscreen mode
  • All ends O(1). Uses: sliding-window maximum, LRU internals.

46. Priority Queue

Theory: Elements dequeued by priority, not insertion order. Backed by a binary heap for
O(log n) insert/extract. (See Min Heap below — a priority queue is a heap with a comparator.)

class PriorityQueue {
  constructor(compare = (a, b) => a - b) {
    this.heap = [];
    this.compare = compare; // min-priority by default
  }
  get size() { return this.heap.length; }
  peek() { return this.heap[0]; }

  enqueue(val) {
    this.heap.push(val);
    this._bubbleUp(this.heap.length - 1);
  }

  dequeue() {
    const top = this.heap[0];
    const last = this.heap.pop();
    if (this.heap.length) { this.heap[0] = last; this._bubbleDown(0); }
    return top;
  }

  _bubbleUp(i) {
    while (i > 0) {
      const parent = (i - 1) >> 1;
      if (this.compare(this.heap[i], this.heap[parent]) >= 0) break;
      [this.heap[i], this.heap[parent]] = [this.heap[parent], this.heap[i]];
      i = parent;
    }
  }

  _bubbleDown(i) {
    const n = this.heap.length;
    while (true) {
      let smallest = i;
      const l = 2 * i + 1, r = 2 * i + 2;
      if (l < n && this.compare(this.heap[l], this.heap[smallest]) < 0) smallest = l;
      if (r < n && this.compare(this.heap[r], this.heap[smallest]) < 0) smallest = r;
      if (smallest === i) break;
      [this.heap[i], this.heap[smallest]] = [this.heap[smallest], this.heap[i]];
      i = smallest;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

47. Min Heap

Theory: Complete binary tree in an array where each parent ≤ children. Parent of i is
(i-1)>>1; children are 2i+1, 2i+2. Insert/extract O(log n), peek O(1).

class MinHeap {
  constructor() { this.heap = []; }
  size() { return this.heap.length; }
  peek() { return this.heap[0]; }

  insert(val) {
    this.heap.push(val);
    let i = this.heap.length - 1;
    // Bubble up while smaller than parent.
    while (i > 0) {
      const p = (i - 1) >> 1;
      if (this.heap[p] <= this.heap[i]) break;
      [this.heap[p], this.heap[i]] = [this.heap[i], this.heap[p]];
      i = p;
    }
  }

  extractMin() {
    if (!this.heap.length) return undefined;
    const min = this.heap[0];
    const last = this.heap.pop();
    if (this.heap.length) {
      this.heap[0] = last;
      this._heapifyDown(0);
    }
    return min;
  }

  _heapifyDown(i) {
    const n = this.heap.length;
    while (true) {
      let smallest = i;
      const l = 2 * i + 1, r = 2 * i + 2;
      if (l < n && this.heap[l] < this.heap[smallest]) smallest = l;
      if (r < n && this.heap[r] < this.heap[smallest]) smallest = r;
      if (smallest === i) break;
      [this.heap[i], this.heap[smallest]] = [this.heap[smallest], this.heap[i]];
      i = smallest;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

48. Max Heap

Theory: Identical to Min Heap with the comparisons flipped (parent ≥ children). A neat trick:
reuse MinHeap by inserting negated numbers, or pass a reversed comparator to PriorityQueue.

class MaxHeap {
  constructor() { this.heap = []; }
  peek() { return this.heap[0]; }
  size() { return this.heap.length; }

  insert(val) {
    this.heap.push(val);
    let i = this.heap.length - 1;
    while (i > 0) {
      const p = (i - 1) >> 1;
      if (this.heap[p] >= this.heap[i]) break; // parent must be LARGER
      [this.heap[p], this.heap[i]] = [this.heap[i], this.heap[p]];
      i = p;
    }
  }

  extractMax() {
    if (!this.heap.length) return undefined;
    const max = this.heap[0];
    const last = this.heap.pop();
    if (this.heap.length) { this.heap[0] = last; this._down(0); }
    return max;
  }

  _down(i) {
    const n = this.heap.length;
    while (true) {
      let largest = i;
      const l = 2 * i + 1, r = 2 * i + 2;
      if (l < n && this.heap[l] > this.heap[largest]) largest = l;
      if (r < n && this.heap[r] > this.heap[largest]) largest = r;
      if (largest === i) break;
      [this.heap[i], this.heap[largest]] = [this.heap[largest], this.heap[i]];
      i = largest;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

49. Trie (Prefix Tree)

Theory: Tree keyed by characters for fast prefix queries. insert/search/startsWith
are O(L) where L = word length. Powers autocomplete and spell-check.

class TrieNode {
  constructor() {
    this.children = {};    // char -> TrieNode
    this.isEnd = false;    // marks a complete word
  }
}

class Trie {
  constructor() { this.root = new TrieNode(); }

  insert(word) {
    let node = this.root;
    for (const ch of word) {
      if (!node.children[ch]) node.children[ch] = new TrieNode();
      node = node.children[ch];
    }
    node.isEnd = true;
  }

  search(word) {
    const node = this._traverse(word);
    return node !== null && node.isEnd; // must be a full word
  }

  startsWith(prefix) {
    return this._traverse(prefix) !== null; // any node is enough
  }

  _traverse(str) {
    let node = this.root;
    for (const ch of str) {
      if (!node.children[ch]) return null;
      node = node.children[ch];
    }
    return node;
  }
}
Enter fullscreen mode Exit fullscreen mode

50. LRU Cache

Theory: Fixed-capacity cache that evicts the Least Recently Used entry. A JS Map
preserves insertion order, so we can get O(1) get/put: on access, delete+reinsert to mark as
most-recent; evict the first (oldest) key when over capacity.

class LRUCache {
  constructor(capacity) {
    this.capacity = capacity;
    this.map = new Map(); // insertion order = usage order (oldest first)
  }

  get(key) {
    if (!this.map.has(key)) return -1;
    const value = this.map.get(key);
    this.map.delete(key);      // remove...
    this.map.set(key, value);  // ...and re-add to mark as most recent
    return value;
  }

  put(key, value) {
    if (this.map.has(key)) this.map.delete(key); // refresh position
    else if (this.map.size >= this.capacity) {
      // Evict least recently used = first key in the Map.
      this.map.delete(this.map.keys().next().value);
    }
    this.map.set(key, value);
  }
}

const lru = new LRUCache(2);
lru.put(1, 1); lru.put(2, 2);
lru.get(1);         // 1 (now 1 is most recent)
lru.put(3, 3);      // evicts key 2
console.log(lru.get(2)); // -1
Enter fullscreen mode Exit fullscreen mode
  • All ops O(1). Follow-up: implement with a hash map + doubly linked list (no Map).

51. LFU Cache

Theory: Evicts the Least Frequently Used entry; ties broken by least-recently-used.
Maintain per-key frequency and, for each frequency, an ordered bucket of keys. O(1) get/put.

class LFUCache {
  constructor(capacity) {
    this.capacity = capacity;
    this.keyToVal = new Map();          // key -> value
    this.keyToFreq = new Map();         // key -> frequency
    this.freqToKeys = new Map();        // freq -> Map(key->true) preserving order
    this.minFreq = 0;
  }

  _touch(key) {
    // Move key from its current freq bucket to freq+1.
    const freq = this.keyToFreq.get(key);
    const bucket = this.freqToKeys.get(freq);
    bucket.delete(key);
    if (bucket.size === 0) {
      this.freqToKeys.delete(freq);
      if (this.minFreq === freq) this.minFreq++; // bumped the lowest bucket
    }
    const nextFreq = freq + 1;
    this.keyToFreq.set(key, nextFreq);
    if (!this.freqToKeys.has(nextFreq)) this.freqToKeys.set(nextFreq, new Map());
    this.freqToKeys.get(nextFreq).set(key, true);
  }

  get(key) {
    if (!this.keyToVal.has(key)) return -1;
    this._touch(key);
    return this.keyToVal.get(key);
  }

  put(key, value) {
    if (this.capacity === 0) return;
    if (this.keyToVal.has(key)) {
      this.keyToVal.set(key, value);
      this._touch(key);
      return;
    }
    if (this.keyToVal.size >= this.capacity) {
      // Evict least-frequent, oldest key (first in the minFreq bucket).
      const bucket = this.freqToKeys.get(this.minFreq);
      const evictKey = bucket.keys().next().value;
      bucket.delete(evictKey);
      this.keyToVal.delete(evictKey);
      this.keyToFreq.delete(evictKey);
    }
    // Insert new key with frequency 1.
    this.keyToVal.set(key, value);
    this.keyToFreq.set(key, 1);
    if (!this.freqToKeys.has(1)) this.freqToKeys.set(1, new Map());
    this.freqToKeys.get(1).set(key, true);
    this.minFreq = 1; // a brand new key always resets minFreq to 1
  }
}
Enter fullscreen mode Exit fullscreen mode

Level 8 — Object Manipulation

52. Flatten Object

Theory: Convert nested objects to a single level using dot-path keys.

function flattenObject(obj, prefix = "", result = {}) {
  for (const key of Object.keys(obj)) {
    const path = prefix ? `${prefix}.${key}` : key;
    const value = obj[key];
    // Recurse into plain nested objects; leaves (incl. arrays) are stored directly.
    if (value !== null && typeof value === "object" && !Array.isArray(value)) {
      flattenObject(value, path, result);
    } else {
      result[path] = value;
    }
  }
  return result;
}

console.log(flattenObject({ a: { b: 1, c: { d: 2 } } }));
// { "a.b": 1, "a.c.d": 2 }
Enter fullscreen mode Exit fullscreen mode
  • Design choice: how to handle arrays (index paths a.0.b vs treat as leaf).

53. Unflatten Object

Theory: Inverse of flatten — rebuild nesting from dot-path keys.

function unflattenObject(flat) {
  const result = {};
  for (const flatKey of Object.keys(flat)) {
    const keys = flatKey.split(".");
    let node = result;
    keys.forEach((key, i) => {
      if (i === keys.length - 1) {
        node[key] = flat[flatKey];      // leaf
      } else {
        node[key] = node[key] || {};    // ensure the branch exists
        node = node[key];
      }
    });
  }
  return result;
}

console.log(unflattenObject({ "a.b": 1, "a.c.d": 2 }));
// { a: { b: 1, c: { d: 2 } } }
Enter fullscreen mode Exit fullscreen mode

54. Deep Equal

Theory: Structural equality — same shape and values recursively (not reference equality).

function deepEqual(a, b) {
  if (a === b) return true; // fast path incl. same reference / same primitive

  // Handle NaN (NaN !== NaN but should be "equal" here).
  if (typeof a === "number" && typeof b === "number") return a !== a && b !== b;

  if (a === null || b === null || typeof a !== "object" || typeof b !== "object") {
    return false;
  }

  // Compare Date and RegExp by their meaningful value.
  if (a instanceof Date && b instanceof Date) return a.getTime() === b.getTime();
  if (a instanceof RegExp && b instanceof RegExp) return a.toString() === b.toString();

  if (Array.isArray(a) !== Array.isArray(b)) return false;

  const keysA = Reflect.ownKeys(a);
  const keysB = Reflect.ownKeys(b);
  if (keysA.length !== keysB.length) return false;

  // Every key in `a` must exist in `b` with a deeply-equal value.
  return keysA.every((k) => keysB.includes(k) && deepEqual(a[k], b[k]));
}

console.log(deepEqual({ a: [1, { b: 2 }] }, { a: [1, { b: 2 }] })); // true
Enter fullscreen mode Exit fullscreen mode
  • Edge cases: NaN, Date, RegExp, arrays vs objects, key count mismatch, cycles (add a seen set).

55. Diff Objects

Theory: Report which keys were added, removed, or changed between two objects.

function diffObjects(oldObj, newObj) {
  const diff = { added: {}, removed: {}, changed: {} };

  for (const key of Object.keys(newObj)) {
    if (!(key in oldObj)) {
      diff.added[key] = newObj[key];
    } else if (!deepEqual(oldObj[key], newObj[key])) {
      diff.changed[key] = { from: oldObj[key], to: newObj[key] };
    }
  }
  for (const key of Object.keys(oldObj)) {
    if (!(key in newObj)) diff.removed[key] = oldObj[key];
  }
  return diff;
}

console.log(diffObjects({ a: 1, b: 2 }, { a: 1, b: 9, c: 3 }));
// { added: { c: 3 }, removed: {}, changed: { b: { from: 2, to: 9 } } }
Enter fullscreen mode Exit fullscreen mode

56. pick

Theory: Return a new object with only the whitelisted keys.

function pick(obj, keys) {
  const result = {};
  for (const key of keys) {
    if (key in obj) result[key] = obj[key];
  }
  return result;
}

console.log(pick({ a: 1, b: 2, c: 3 }, ["a", "c"])); // { a: 1, c: 3 }
Enter fullscreen mode Exit fullscreen mode

57. omit

Theory: Return a new object without the blacklisted keys.

function omit(obj, keys) {
  const exclude = new Set(keys);
  const result = {};
  for (const key of Object.keys(obj)) {
    if (!exclude.has(key)) result[key] = obj[key];
  }
  return result;
}

console.log(omit({ a: 1, b: 2, c: 3 }, ["b"])); // { a: 1, c: 3 }
Enter fullscreen mode Exit fullscreen mode

Level 9 — Parsing

58. Query String Parser

Theory: Turn ?a=1&b=2 into { a: "1", b: "2" }. Handle URL-decoding and repeated keys.

function parseQuery(queryString) {
  const result = {};
  // Strip a leading "?" if present, then split into pairs.
  const query = queryString.replace(/^\?/, "");
  if (!query) return result;

  for (const pair of query.split("&")) {
    const [rawKey, rawValue = ""] = pair.split("=");
    const key = decodeURIComponent(rawKey);
    const value = decodeURIComponent(rawValue);

    if (key in result) {
      // Repeated key → collect into an array.
      result[key] = [].concat(result[key], value);
    } else {
      result[key] = value;
    }
  }
  return result;
}

console.log(parseQuery("?a=1&b=2&a=3"));
// { a: ["1", "3"], b: "2" }
Enter fullscreen mode Exit fullscreen mode

59. Stringify Query

Theory: Inverse of the above. URL-encode keys/values and expand arrays into repeated keys.

function stringifyQuery(obj) {
  const parts = [];
  for (const key of Object.keys(obj)) {
    const value = obj[key];
    const enc = encodeURIComponent(key);
    if (Array.isArray(value)) {
      value.forEach((v) => parts.push(`${enc}=${encodeURIComponent(v)}`));
    } else {
      parts.push(`${enc}=${encodeURIComponent(value)}`);
    }
  }
  return parts.length ? "?" + parts.join("&") : "";
}

console.log(stringifyQuery({ a: [1, 3], b: 2 })); // "?a=1&a=3&b=2"
Enter fullscreen mode Exit fullscreen mode

60. Parse CSV

Theory: Split rows/columns, but respect quoted fields that may contain commas, quotes
(escaped as ""), and newlines. A tiny state machine is the robust approach.

function parseCSV(text) {
  const rows = [];
  let row = [];
  let field = "";
  let inQuotes = false;

  for (let i = 0; i < text.length; i++) {
    const char = text[i];
    const next = text[i + 1];

    if (inQuotes) {
      if (char === '"' && next === '"') { field += '"'; i++; } // escaped quote
      else if (char === '"') { inQuotes = false; }             // closing quote
      else { field += char; }
    } else {
      if (char === '"') inQuotes = true;
      else if (char === ",") { row.push(field); field = ""; }
      else if (char === "\n" || char === "\r") {
        if (char === "\r" && next === "\n") i++; // consume CRLF pair
        row.push(field); rows.push(row); row = []; field = "";
      } else field += char;
    }
  }
  // Flush the trailing field/row if the file doesn't end with a newline.
  if (field !== "" || row.length) { row.push(field); rows.push(row); }
  return rows;
}

console.log(parseCSV('a,b\n"1,x",2')); // [["a","b"],["1,x","2"]]
Enter fullscreen mode Exit fullscreen mode

61. Parse JSON (simplified)

Theory: A recursive-descent parser demonstrating tokenizing + parsing. Supports objects,
arrays, strings, numbers, booleans, null. (Real JSON.parse handles all escapes/edge cases.)

function parseJSON(str) {
  let i = 0;

  function skipWhitespace() { while (/\s/.test(str[i])) i++; }

  function parseValue() {
    skipWhitespace();
    const ch = str[i];
    if (ch === "{") return parseObject();
    if (ch === "[") return parseArray();
    if (ch === '"') return parseString();
    if (ch === "t" || ch === "f") return parseBool();
    if (ch === "n") { i += 4; return null; } // "null"
    return parseNumber();
  }

  function parseObject() {
    const obj = {};
    i++; // skip "{"
    skipWhitespace();
    if (str[i] === "}") { i++; return obj; }
    while (true) {
      skipWhitespace();
      const key = parseString();
      skipWhitespace();
      i++; // skip ":"
      obj[key] = parseValue();
      skipWhitespace();
      if (str[i] === ",") { i++; continue; }
      i++; // skip "}"
      break;
    }
    return obj;
  }

  function parseArray() {
    const arr = [];
    i++; // skip "["
    skipWhitespace();
    if (str[i] === "]") { i++; return arr; }
    while (true) {
      arr.push(parseValue());
      skipWhitespace();
      if (str[i] === ",") { i++; continue; }
      i++; // skip "]"
      break;
    }
    return arr;
  }

  function parseString() {
    let result = "";
    i++; // skip opening quote
    while (str[i] !== '"') {
      if (str[i] === "\\") { i++; result += str[i]; } // naive escape handling
      else result += str[i];
      i++;
    }
    i++; // skip closing quote
    return result;
  }

  function parseNumber() {
    let start = i;
    while (/[-+0-9.eE]/.test(str[i])) i++;
    return Number(str.slice(start, i));
  }

  function parseBool() {
    if (str[i] === "t") { i += 4; return true; }
    i += 5; return false;
  }

  return parseValue();
}

console.log(parseJSON('{"a":1,"b":[true,null]}')); // { a: 1, b: [true, null] }
Enter fullscreen mode Exit fullscreen mode

Level 10 — Tree / DOM Problems

62. DOM Traversal (DFS)

Theory: Visit every node depth-first (node, then its children recursively).

function domDFS(root, visit) {
  visit(root);
  // childNodes includes text nodes; use `children` for elements only.
  for (const child of root.children) {
    domDFS(child, visit);
  }
}

// Iterative version using a stack (avoids deep recursion).
function domDFSIterative(root, visit) {
  const stack = [root];
  while (stack.length) {
    const node = stack.pop();
    visit(node);
    // Push in reverse so children are visited left-to-right.
    for (let i = node.children.length - 1; i >= 0; i--) {
      stack.push(node.children[i]);
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

63. BFS DOM

Theory: Visit level by level using a queue. Useful for "find nearest element" style problems.

function domBFS(root, visit) {
  const queue = [root];
  while (queue.length) {
    const node = queue.shift();
    visit(node);
    for (const child of node.children) queue.push(child);
  }
}

// Common application: get max nesting depth.
function maxDepth(root) {
  if (!root) return 0;
  let depth = 0;
  let queue = [root];
  while (queue.length) {
    depth++;
    const next = [];
    for (const node of queue) next.push(...node.children);
    queue = next;
  }
  return depth;
}
Enter fullscreen mode Exit fullscreen mode

64. Virtual DOM Diff (basic)

Theory: Compare two virtual node trees and produce a list of patches. A vnode is
{ type, props, children }. This is the heart of React's reconciliation (simplified —
real diffing uses keys and heuristics).

// vnode: { type: 'div', props: {...}, children: [...] }

function diff(oldNode, newNode) {
  const patches = [];

  // 1. Node removed.
  if (newNode == null) {
    patches.push({ type: "REMOVE" });
    return patches;
  }
  // 2. Text or type changed → full replace.
  if (
    typeof oldNode !== typeof newNode ||
    (typeof newNode === "string" && oldNode !== newNode) ||
    oldNode.type !== newNode.type
  ) {
    patches.push({ type: "REPLACE", node: newNode });
    return patches;
  }
  // 3. Same element type → diff props and children.
  if (newNode.type) {
    const propPatches = diffProps(oldNode.props || {}, newNode.props || {});
    if (propPatches.length) patches.push({ type: "PROPS", patches: propPatches });

    const childPatches = diffChildren(oldNode.children || [], newNode.children || []);
    if (childPatches.length) patches.push({ type: "CHILDREN", patches: childPatches });
  }
  return patches;
}

function diffProps(oldProps, newProps) {
  const changes = [];
  // Changed or added props.
  for (const key of Object.keys(newProps)) {
    if (oldProps[key] !== newProps[key]) changes.push({ key, value: newProps[key] });
  }
  // Removed props.
  for (const key of Object.keys(oldProps)) {
    if (!(key in newProps)) changes.push({ key, value: undefined });
  }
  return changes;
}

function diffChildren(oldChildren, newChildren) {
  const patches = [];
  const max = Math.max(oldChildren.length, newChildren.length);
  for (let i = 0; i < max; i++) {
    patches.push({ index: i, patches: diff(oldChildren[i], newChildren[i]) });
  }
  return patches;
}
Enter fullscreen mode Exit fullscreen mode
  • Follow-up: keyed diffing for lists (key prop) to minimize DOM moves.

65. Render JSON → DOM

Theory: Turn a vnode/JSON tree into real DOM nodes (the "mount" step).

function createElement(vnode) {
  // Text node.
  if (typeof vnode === "string" || typeof vnode === "number") {
    return document.createTextNode(String(vnode));
  }

  const el = document.createElement(vnode.type);

  // Apply props/attributes and event handlers.
  for (const [key, value] of Object.entries(vnode.props || {})) {
    if (key.startsWith("on") && typeof value === "function") {
      el.addEventListener(key.slice(2).toLowerCase(), value); // onClick -> click
    } else {
      el.setAttribute(key, value);
    }
  }

  // Recursively mount children.
  (vnode.children || []).forEach((child) => el.appendChild(createElement(child)));
  return el;
}

// const tree = { type: 'div', props: { id: 'app' }, children: ['Hello'] };
// document.body.appendChild(createElement(tree));
Enter fullscreen mode Exit fullscreen mode

Level 11 — Rate Limiting

66. Token Bucket

Theory: A bucket holds up to capacity tokens, refilled at refillRate tokens/sec.
Each request consumes one token; if empty, the request is rejected. Allows bursts up to
capacity — the classic API rate limiter.

class TokenBucket {
  constructor(capacity, refillRatePerSec) {
    this.capacity = capacity;
    this.tokens = capacity;              // start full
    this.refillRate = refillRatePerSec;
    this.lastRefill = Date.now();
  }

  _refill() {
    const now = Date.now();
    const elapsedSec = (now - this.lastRefill) / 1000;
    // Add tokens proportional to elapsed time, capped at capacity.
    this.tokens = Math.min(this.capacity, this.tokens + elapsedSec * this.refillRate);
    this.lastRefill = now;
  }

  tryConsume(count = 1) {
    this._refill();
    if (this.tokens >= count) {
      this.tokens -= count;
      return true;  // allowed
    }
    return false;   // rate limited
  }
}

const bucket = new TokenBucket(10, 1); // 10 burst, refill 1/sec
console.log(bucket.tryConsume()); // true
Enter fullscreen mode Exit fullscreen mode

67. Sliding Window (Rate Limiter)

Theory: Allow at most limit requests within the trailing windowMs. Track timestamps and
drop those older than the window — smoother than fixed windows (no burst at boundaries).

class SlidingWindowRateLimiter {
  constructor(limit, windowMs) {
    this.limit = limit;
    this.windowMs = windowMs;
    this.timestamps = []; // request times within the window
  }

  allow() {
    const now = Date.now();
    const cutoff = now - this.windowMs;
    // Drop expired timestamps from the front.
    while (this.timestamps.length && this.timestamps[0] <= cutoff) {
      this.timestamps.shift();
    }
    if (this.timestamps.length < this.limit) {
      this.timestamps.push(now);
      return true;
    }
    return false;
  }
}

const rl = new SlidingWindowRateLimiter(3, 1000); // 3 per second
Enter fullscreen mode Exit fullscreen mode

68. Request Queue (rate-limited)

Theory: Queue requests and release them at a controlled rate so you never exceed the limit —
requests wait rather than fail.

class RequestQueue {
  constructor(maxPerInterval, intervalMs) {
    this.maxPerInterval = maxPerInterval;
    this.intervalMs = intervalMs;
    this.queue = [];
    this.timestamps = [];
  }

  add(taskFn) {
    return new Promise((resolve, reject) => {
      this.queue.push({ taskFn, resolve, reject });
      this._process();
    });
  }

  _process() {
    const now = Date.now();
    this.timestamps = this.timestamps.filter((t) => now - t < this.intervalMs);

    if (this.timestamps.length < this.maxPerInterval && this.queue.length) {
      this.timestamps.push(now);
      const { taskFn, resolve, reject } = this.queue.shift();
      Promise.resolve(taskFn()).then(resolve, reject);
      this._process(); // try to release more within budget
    } else if (this.queue.length) {
      // Budget exhausted — retry after the oldest timestamp expires.
      const wait = this.intervalMs - (now - this.timestamps[0]);
      setTimeout(() => this._process(), wait);
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Level 12 — Advanced Utilities

69. groupBy

Theory: Group array items into buckets keyed by a function or property name.

function groupBy(array, keySelector) {
  const getKey = typeof keySelector === "function"
    ? keySelector
    : (item) => item[keySelector];

  return array.reduce((groups, item) => {
    const key = getKey(item);
    (groups[key] ||= []).push(item); // create bucket lazily
    return groups;
  }, {});
}

console.log(groupBy([6.1, 4.2, 6.3], Math.floor)); // { 4: [4.2], 6: [6.1, 6.3] }
console.log(groupBy([{ t: "a" }, { t: "b" }, { t: "a" }], "t"));
// { a: [{t:"a"},{t:"a"}], b: [{t:"b"}] }
Enter fullscreen mode Exit fullscreen mode

70. chunk()

Theory: Split an array into groups of size n (last chunk may be smaller).

function chunk(array, size) {
  if (size <= 0) throw new RangeError("size must be > 0");
  const result = [];
  for (let i = 0; i < array.length; i += size) {
    result.push(array.slice(i, i + size)); // slice handles the tail gracefully
  }
  return result;
}

console.log(chunk([1, 2, 3, 4, 5], 2)); // [[1,2],[3,4],[5]]
Enter fullscreen mode Exit fullscreen mode

71. zip()

Theory: Combine multiple arrays element-wise into tuples. Length = shortest input (or longest,
padded — state your choice).

function zip(...arrays) {
  const maxLen = Math.min(...arrays.map((a) => a.length));
  const result = [];
  for (let i = 0; i < maxLen; i++) {
    result.push(arrays.map((arr) => arr[i]));
  }
  return result;
}

console.log(zip([1, 2, 3], ["a", "b", "c"])); // [[1,"a"],[2,"b"],[3,"c"]]
Enter fullscreen mode Exit fullscreen mode

72. unzip()

Theory: Inverse of zip — turn an array of tuples back into separate arrays.

function unzip(pairs) {
  const result = [];
  for (const tuple of pairs) {
    tuple.forEach((value, i) => {
      (result[i] ||= []).push(value);
    });
  }
  return result;
}

console.log(unzip([[1, "a"], [2, "b"], [3, "c"]])); // [[1,2,3],["a","b","c"]]
Enter fullscreen mode Exit fullscreen mode

73. shuffle()

Theory: Fisher–Yates produces a uniformly random permutation in O(n). Naive sort(() =>
Math.random() - 0.5)
is biased — avoid it.

function shuffle(array) {
  const arr = [...array]; // don't mutate the input
  for (let i = arr.length - 1; i > 0; i--) {
    // Pick a random index j in [0, i], then swap.
    const j = Math.floor(Math.random() * (i + 1));
    [arr[i], arr[j]] = [arr[j], arr[i]];
  }
  return arr;
}
Enter fullscreen mode Exit fullscreen mode

74. sample()

Theory: Pick k random elements without replacement (partial Fisher–Yates is efficient).

function sample(array, k = 1) {
  const arr = [...array];
  const n = Math.min(k, arr.length);
  for (let i = 0; i < n; i++) {
    const j = i + Math.floor(Math.random() * (arr.length - i));
    [arr[i], arr[j]] = [arr[j], arr[i]];
  }
  return arr.slice(0, n);
}

console.log(sample([1, 2, 3, 4, 5], 2)); // e.g. [3, 1]
Enter fullscreen mode Exit fullscreen mode

75. binarySearch()

Theory: Find a target in a sorted array in O(log n) by halving the search range.

function binarySearch(sortedArr, target) {
  let lo = 0, hi = sortedArr.length - 1;
  while (lo <= hi) {
    const mid = (lo + hi) >> 1; // avoids overflow, integer division
    if (sortedArr[mid] === target) return mid;
    if (sortedArr[mid] < target) lo = mid + 1; // search right half
    else hi = mid - 1;                          // search left half
  }
  return -1; // not found
}

console.log(binarySearch([1, 3, 5, 7, 9], 7)); // 3
Enter fullscreen mode Exit fullscreen mode
  • Variations: find leftmost/rightmost occurrence, first element ≥ target (lower bound).

76. quickSort()

Theory: Divide & conquer: pick a pivot, partition into <, =, >, recurse. Average
O(n log n), worst O(n²) (mitigated by random pivot).

function quickSort(arr) {
  if (arr.length <= 1) return arr;

  const pivot = arr[Math.floor(Math.random() * arr.length)]; // random pivot
  const less = [], equal = [], greater = [];

  for (const x of arr) {
    if (x < pivot) less.push(x);
    else if (x > pivot) greater.push(x);
    else equal.push(x);
  }
  // Recurse on the partitions and concatenate.
  return [...quickSort(less), ...equal, ...quickSort(greater)];
}

console.log(quickSort([3, 1, 4, 1, 5, 9, 2, 6])); // [1,1,2,3,4,5,6,9]
Enter fullscreen mode Exit fullscreen mode
  • Note: the above is not in-place. Mention the Lomuto/Hoare in-place partition as a follow-up.

77. mergeSort()

Theory: Divide the array in half, sort each recursively, then merge. Stable, guaranteed
O(n log n), O(n) extra space.

function mergeSort(arr) {
  if (arr.length <= 1) return arr;
  const mid = arr.length >> 1;
  const left = mergeSort(arr.slice(0, mid));
  const right = mergeSort(arr.slice(mid));
  return merge(left, right);
}

function merge(left, right) {
  const result = [];
  let i = 0, j = 0;
  // Merge two sorted arrays by always taking the smaller head.
  while (i < left.length && j < right.length) {
    if (left[i] <= right[j]) result.push(left[i++]); // <= keeps it stable
    else result.push(right[j++]);
  }
  // Append leftovers.
  while (i < left.length) result.push(left[i++]);
  while (j < right.length) result.push(right[j++]);
  return result;
}

console.log(mergeSort([5, 2, 8, 1, 9])); // [1,2,5,8,9]
Enter fullscreen mode Exit fullscreen mode

78. heapSort()

Theory: Build a max-heap, then repeatedly swap the max to the end and sift down.
In-place, O(n log n), not stable.

function heapSort(arr) {
  const n = arr.length;

  // Build max-heap from the last parent down to the root.
  for (let i = (n >> 1) - 1; i >= 0; i--) siftDown(arr, i, n);

  // Repeatedly move current max to the end, then restore the heap.
  for (let end = n - 1; end > 0; end--) {
    [arr[0], arr[end]] = [arr[end], arr[0]];
    siftDown(arr, 0, end); // heap size shrinks to `end`
  }
  return arr;
}

function siftDown(arr, i, size) {
  while (true) {
    let largest = i;
    const l = 2 * i + 1, r = 2 * i + 2;
    if (l < size && arr[l] > arr[largest]) largest = l;
    if (r < size && arr[r] > arr[largest]) largest = r;
    if (largest === i) break;
    [arr[i], arr[largest]] = [arr[largest], arr[i]];
    i = largest;
  }
}

console.log(heapSort([3, 1, 4, 1, 5, 9])); // [1,1,3,4,5,9]
Enter fullscreen mode Exit fullscreen mode

79. topK()

Theory: Find the K largest elements. A min-heap of size K gives O(n log K) — better than
sorting (O(n log n)) when K ≪ n.

function topK(nums, k) {
  const heap = new MinHeap(); // from Level 7
  for (const num of nums) {
    heap.insert(num);
    // Keep only the K largest: if heap grows past K, drop the smallest.
    if (heap.size() > k) heap.extractMin();
  }
  const result = [];
  while (heap.size()) result.push(heap.extractMin());
  return result.reverse(); // largest first
}

console.log(topK([3, 1, 5, 12, 2, 11], 3)); // [12, 11, 5]
Enter fullscreen mode Exit fullscreen mode
  • Alternative: Quickselect gives O(n) average for the k-th element.

80. median()

Theory: Middle value of a sorted list; average of the two middles for even length.
Simple version sorts (O(n log n)); Quickselect gives O(n) average.

function median(nums) {
  if (nums.length === 0) return NaN;
  const sorted = [...nums].sort((a, b) => a - b); // numeric sort, no mutation
  const mid = sorted.length >> 1;
  return sorted.length % 2 === 0
    ? (sorted[mid - 1] + sorted[mid]) / 2 // even: average the two middles
    : sorted[mid];                        // odd: the middle element
}

console.log(median([3, 1, 2]));    // 2
console.log(median([4, 1, 2, 3])); // 2.5
Enter fullscreen mode Exit fullscreen mode
  • Streaming variation: maintain a max-heap (lower half) + min-heap (upper half) for O(log n) insertions and O(1) median — the classic "find median from data stream" problem.

Level 13 — Google Frontend Machine Coding

The Level 1–12 building blocks map directly onto these classic Google frontend rounds. Below are
the production-grade versions of the ones most likely to appear.

81. Production Debounce (leading, trailing, maxWait)

Theory: Real debounce (à la Lodash) supports firing on the leading edge, trailing edge,
and a maxWait so a continuously-called function still fires periodically.

function debounce(fn, wait, options = {}) {
  const { leading = false, trailing = true, maxWait } = options;
  let timerId = null;
  let lastCallTime = null;   // when debounced was last invoked
  let lastInvokeTime = 0;    // when fn was last actually called
  let lastArgs, lastThis, result;

  function invoke(time) {
    lastInvokeTime = time;
    result = fn.apply(lastThis, lastArgs);
    lastArgs = lastThis = null;
    return result;
  }

  function shouldInvoke(time) {
    const sinceLastCall = time - lastCallTime;
    const sinceLastInvoke = time - lastInvokeTime;
    return (
      lastCallTime === null ||           // first call
      sinceLastCall >= wait ||           // idle long enough
      sinceLastCall < 0 ||               // clock moved back
      (maxWait !== undefined && sinceLastInvoke >= maxWait) // maxWait exceeded
    );
  }

  function timerExpired() {
    const time = Date.now();
    if (shouldInvoke(time)) {
      timerId = null;
      if (trailing && lastArgs) invoke(time); // trailing edge
      return;
    }
    // Reschedule for the remaining time.
    timerId = setTimeout(timerExpired, wait - (time - lastCallTime));
  }

  function debounced(...args) {
    const time = Date.now();
    const isInvoking = shouldInvoke(time);
    lastArgs = args;
    lastThis = this;
    lastCallTime = time;

    if (isInvoking && timerId === null) {
      lastInvokeTime = time;
      timerId = setTimeout(timerExpired, wait);
      return leading ? invoke(time) : result; // leading edge
    }
    if (timerId === null) timerId = setTimeout(timerExpired, wait);
    return result;
  }

  debounced.cancel = () => { clearTimeout(timerId); timerId = null; lastCallTime = null; };
  debounced.flush = () => { if (timerId) { invoke(Date.now()); debounced.cancel(); } return result; };
  return debounced;
}
Enter fullscreen mode Exit fullscreen mode
  • Talking points: leading vs trailing, maxWait guarantees progress, cancel/flush API.

82. Promise Scheduler with Concurrency Limit

Covered as Promise Queue (#33) and
Task Scheduler (#34). The interviewer usually wants:
new Scheduler(limit) with .add(taskFn) → Promise that resolves with the task's result while
never running more than limit tasks at once. Use the Promise Queue implementation.


83. Retry with Exponential Backoff

Theory: On failure, wait base * 2^attempt (plus optional jitter to avoid thundering herds)
before retrying. Standard for flaky network calls.

async function retryWithBackoff(fn, {
  retries = 5,
  baseDelay = 200,
  maxDelay = 5000,
  jitter = true,
} = {}) {
  let attempt = 0;
  while (true) {
    try {
      return await fn(); // success → return immediately
    } catch (err) {
      if (attempt >= retries) throw err; // exhausted → rethrow

      // Exponential delay, capped, with optional random jitter.
      let delay = Math.min(baseDelay * 2 ** attempt, maxDelay);
      if (jitter) delay = Math.random() * delay;

      await new Promise((r) => setTimeout(r, delay));
      attempt++;
    }
  }
}

// retryWithBackoff(() => fetch("/flaky"), { retries: 4 });
Enter fullscreen mode Exit fullscreen mode

84. Flatten Deeply Nested Array Without Recursion

Use the explicit-stack approach from flat (#7). Restated:

function flattenNoRecursion(arr) {
  const stack = [...arr];
  const out = [];
  while (stack.length) {
    const next = stack.pop();
    if (Array.isArray(next)) stack.push(...next); // defer nested items
    else out.push(next);
  }
  return out.reverse(); // pop reverses; restore original order
}

console.log(flattenNoRecursion([1, [2, [3, [4, [5]]]]])); // [1,2,3,4,5]
Enter fullscreen mode Exit fullscreen mode

85. DOM Serialization / Deserialization

Theory: Serialize a DOM node to a JSON vnode and back — round-tripping between the DOM and a
virtual representation.

// DOM element -> plain JSON vnode.
function serialize(node) {
  if (node.nodeType === Node.TEXT_NODE) return node.textContent;
  const props = {};
  for (const attr of node.attributes) props[attr.name] = attr.value;
  return {
    type: node.tagName.toLowerCase(),
    props,
    children: Array.from(node.childNodes).map(serialize),
  };
}

// JSON vnode -> DOM element (reuses createElement from #65).
function deserialize(vnode) {
  if (typeof vnode === "string") return document.createTextNode(vnode);
  const el = document.createElement(vnode.type);
  for (const [k, v] of Object.entries(vnode.props || {})) el.setAttribute(k, v);
  (vnode.children || []).forEach((c) => el.appendChild(deserialize(c)));
  return el;
}
Enter fullscreen mode Exit fullscreen mode

Quick Reference — Complexity Cheat Sheet

Utility Time (avg) Space Notes
map/filter/reduce O(n) O(n) single pass
deepClone O(n) O(n) WeakMap for cycles
debounce/throttle O(1)/call O(1) one timer
Promise.all O(n) O(n) fail-fast
LRU get/put O(1) O(cap) Map ordering trick
Heap insert/extract O(log n) O(n) array-backed
Trie op O(L) O(Σ·N) L = word length
binarySearch O(log n) O(1) requires sorted input
quick/merge/heap O(n log n) varies merge stable, heap in-place
topK O(n log k) O(k) min-heap of size k

30-Day Practice Plan

  • Week 1 — Fundamentals: Array methods (1–10), object utilities (11–15), bind/call/apply (24–26), debounce (16, 81), throttle (17).
  • Week 2 — Async & core: Promises (27–34), async utilities (35–39), EventEmitter (40), deep clone (10), deep equality (54).
  • Week 3 — Data structures: LRU (50) / LFU (51), heaps (47–48), tries (49), flatten/unflatten (52–53), parsers (58–61), rate limiters (66–68).
  • Week 4 — Machine coding: Promise scheduler (33–34), virtual DOM diff (64), task queue (33), DOM traversal (62–63), retry/backoff (83) — then do timed mock interviews.

Rule: write each from scratch, state complexity, enumerate edge cases, and test it —
no peeking. That is exactly what a Google frontend loop expects.

Top comments (0)