Back to Notes

JS Machine Coding Patterns

The recurring "implement this from scratch" set — expect at least one of these in any JS machine-coding round.

Debounce vs. Throttle

Debounce: run only after the trigger has paused for delay ms — good for search-input, resize. Throttle: run at most once every limit ms — good for scroll, mousemove.

function debounce(func, delay) {
  let timerId;
  function debounced(...args) {
    clearTimeout(timerId);
    timerId = setTimeout(() => func.apply(this, args), delay);
  }
  debounced.cancel = () => clearTimeout(timerId);
  debounced.flush = () => { /* invoke immediately if pending — track args/this to support this */ };
  return debounced;
}

function throttle(func, limit) {
  let throttled = false;
  return function(...args) {
    if (throttled) return;
    func.apply(this, args);
    throttled = true;
    setTimeout(() => { throttled = false; }, limit);
  };
}

One-liner to remember: debounce is "run after the pause"; throttle is "run at the start, then wait."

Currying

function curry(func) {
  return function curried(...args) {
    if (args.length >= func.length) return func.apply(this, args);
    return (...more) => curried.apply(this, args.concat(more));
  };
}

// Infinite currying (sum that's also usable as a plain number via valueOf)
function sum(val) {
  let total = val;
  function inner(next) { total += next; return inner; }
  inner.valueOf = () => total;
  return inner;
}
sum(1)(2)(3) == 6;   // true — valueOf is invoked implicitly in a numeric comparison context

func.length (the declared arity of the original function) is what tells the generic curry implementation how many arguments to wait for before actually invoking — this is the key insight that makes a generic curry function possible, rather than one hardcoded to a specific argument count.

Promise Polyfills

function promiseAll(iterable) {
  return new Promise((resolve, reject) => {
    const result = new Array(iterable.length);
    let completed = 0;
    if (iterable.length === 0) return resolve([]);
    iterable.forEach((item, i) => {
      Promise.resolve(item).then(value => {
        result[i] = value;
        if (++completed === iterable.length) resolve(result);
      }).catch(reject);
    });
  });
}

function promiseAllSettled(iterable) {
  return new Promise(resolve => {
    const result = new Array(iterable.length);
    let completed = 0;
    if (iterable.length === 0) return resolve([]);
    iterable.forEach((item, i) => {
      Promise.resolve(item)
        .then(value => { result[i] = { status: "fulfilled", value }; })
        .catch(reason => { result[i] = { status: "rejected", reason }; })
        .finally(() => { if (++completed === iterable.length) resolve(result); });
    });
  });
}

function promiseRace(iterable) {
  return new Promise((resolve, reject) => {
    iterable.forEach(item => Promise.resolve(item).then(resolve, reject));
  });
}

Note promiseAll's .catch(reject) propagates the first rejection immediately (fail-fast), while allSettled's .finally always increments completed regardless of fulfillment/rejection — that structural difference (reject-and-stop vs. record-and-continue) is the entire semantic distinction between the two combinators, implemented in a few lines.

Array Method Polyfills

Array.prototype.myMap = function(callback, thisArg) {
  const result = new Array(this.length);
  for (let i = 0; i < this.length; i++) {
    if (Object.hasOwn(this, i)) result[i] = callback.call(thisArg, this[i], i, this);
  }
  return result;
};

Array.prototype.myReduce = function(callback, initialVal) {
  let result = initialVal;
  let i = 0;
  if (result === undefined) {
    if (this.length === 0) throw new TypeError("Reduce of empty array with no initial value");
    result = this[0]; i = 1;
  }
  for (; i < this.length; i++) result = callback(result, this[i], i, this);
  return result;
};

Object.hasOwn(this, i) (checking for array "holes," see [[JS Core Language & Data Types]]) is what makes these polyfills correctly skip sparse-array gaps the same way the native methods do, rather than treating a hole as undefined.

Deep Clone

// Basic — JSON-serializable values only
function deepClone(value) {
  if (typeof value !== "object" || value === null) return value;
  if (Array.isArray(value)) return value.map(deepClone);
  return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, deepClone(v)]));
}

// Handles cycles + Date/RegExp/Map/Set — the real interview follow-up
function deepCloneWithCache(value, cache = new WeakMap()) {
  if (typeof value !== "object" || value === null) return value;
  if (cache.has(value)) return cache.get(value);          // cycle detected — return the already-cloned ref

  if (value instanceof Date) return new Date(value);
  if (value instanceof RegExp) return new RegExp(value);

  const cloned = Array.isArray(value) ? [] : Object.create(Object.getPrototypeOf(value));
  cache.set(value, cloned);    // register BEFORE recursing — this is what breaks infinite cycles
  for (const key of Reflect.ownKeys(value)) {
    cloned[key] = deepCloneWithCache(value[key], cache);
  }
  return cloned;
}

Why the cache must be set before recursing into children, not after: if object A references itself (directly or via B), the recursive call into A's own children needs to find A's clone already registered in the cache — registering it after fully cloning would mean the recursive call never terminates, since it'd try to clone A again from scratch every time it's encountered.

Custom getElementsByClassName (DOM Traversal)

function getElementsByClassName(element, classNames) {
  const elements = [];
  const classes = new Set(classNames.trim().split(/\s+/));
  function helper(el) {
    if ([...classes].every(c => el.classList.contains(c))) elements.push(el);
    for (const child of el.children) helper(child);
  }
  for (const child of element.children) helper(child);
  return elements;
}

A plain recursive tree walk — the same divide-and-conquer shape as most custom DOM-traversal machine-coding questions (getElementsByTagName, getElementsByStyle follow an identical structure, just swapping the match condition).

Interview Drills

  1. How does a generic curry(func) implementation know how many arguments to collect before invoking the original function?func.length gives the declared number of parameters of the original function — the curried wrapper compares the accumulated argument count against this to decide whether to invoke now or return another partially-applied function waiting for more arguments.
  2. In a cycle-safe deepClone, why must the new clone be registered in the cache before recursing into its children? — If object A (directly or transitively) references itself, the recursive call encountering A again needs to find its already-in-progress clone in the cache and return that reference immediately — registering only after the clone fully completes would mean a truly cyclic structure never terminates, since each encounter would restart cloning from scratch.
  3. Why does Promise.all's polyfill call reject on the first rejection while allSettled's polyfill never rejects at all? — This is the entire semantic difference between the two combinators, not an implementation detail — all is fail-fast (any single rejection aborts the whole combined promise), while allSettled guarantees it always resolves once every input has settled, successful or not, which is exactly why its per-item handler always increments the completed counter in a .finally() rather than propagating a rejection.

Cross-links

[[JS Core Language & Data Types]] · [[JS Closures & Async]] · [[OOD Design Patterns]]