These are the "implement X" questions. Interviewers use them because the interface is known and the edge cases are not: this, argument capture, cyclic references, Map and Date values, cancellation, memory. The senior answer knows which built-in already exists, writes the rest correctly, and says what it would test.
this preserved, cancel/flush, leading and trailing options, timer cleanup on component unmount (VueUse's useDebounceFn handles the Vue lifecycle).Map (supports object keys by identity), multi-argument with a serialiser or a nested Map trie; WeakMap for object keys to avoid leaks; bounded size (LRU) for long-running apps; async memoize must cache the promise and evict on rejection.NaN equals NaN; arrays by index; Date by time; RegExp by source and flags; Map and Set by entries; plain objects by own enumerable keys (including symbol keys if required); cycles with a WeakMap of visited pairs; +0 vs -0 per requirement.structuredClone (handles cycles, Date, Map, Set, typed arrays; throws on functions and DOM nodes, drops prototypes and getters). A hand-written version is asked to see recursion, cycle handling with a WeakMap, and special types.undefined does not overwrite; guard against prototype pollution by skipping __proto__, constructor and prototype keys; defu is the Nuxt-world library (defaults merging, right-to-left).Object.groupBy and Map.groupBy are built in (ES2024); chunk is a loop with slice; uniqBy is a Set of seen keys; Array.prototype.at, toSorted, toReversed, with, findLast are the modern immutable helpers.Pick<T, K> and Omit<T, K>; Object.fromEntries plus filter; note that omit preserves prototype-less copies only.on, off, once, emit; store listeners in a Map<string, Set<Listener>>; return an unsubscribe function; copy the set before iterating so handlers can unsubscribe during emit; typed events with a generic map (mitt is the tiny library; Nuxt's hooks use hookable).Map preserves insertion order, so get deletes and re-inserts to mark recent, set evicts map.keys().next().value when over capacity; O(1) operations. Ask about TTL and size in bytes.AbortSignal support; covered in the async page.structuredClone, Object.groupBy, Array.fromAsync, Promise.withResolvers, Set methods (union, intersection, difference), Iterator helpers (map, filter, take, toArray), Intl.* for formatting, crypto.randomUUID, AbortSignal.timeout and AbortSignal.any, Error cause, using declarations (ES2026 explicit resource management) for disposables.Throttle with leading and trailing invocations:
export function throttle<T extends (...args: any[]) => void>(fn: T, wait: number, { leading = true, trailing = true } = {}) {
let last = 0
let timer: ReturnType<typeof setTimeout> | undefined
let pending: Parameters<T> | undefined
return function (this: unknown, ...args: Parameters<T>) {
const now = Date.now()
if (!last && !leading) last = now
const remaining = wait - (now - last)
if (remaining <= 0) {
clearTimeout(timer); timer = undefined
last = now
fn.apply(this, args)
}
else if (trailing) {
pending = args
timer ??= setTimeout(() => {
last = leading ? Date.now() : 0
timer = undefined
if (pending) fn.apply(this, pending)
pending = undefined
}, remaining)
}
}
}
Memoize with a WeakMap for object keys and a Map for primitives, plus an async variant:
export function memoize<A extends object | string | number, R>(fn: (arg: A) => R) {
const objects = new WeakMap<object, R>()
const primitives = new Map<string | number, R>()
return (arg: A): R => {
const store = typeof arg === 'object' ? objects : primitives
if (store.has(arg as never)) return store.get(arg as never)!
const result = fn(arg)
store.set(arg as never, result)
return result
}
}
export function memoizeAsync<A extends string | number, R>(fn: (arg: A) => Promise<R>) {
const cache = new Map<A, Promise<R>>()
return (arg: A) => {
const hit = cache.get(arg)
if (hit) return hit
const p = fn(arg).catch((err) => { cache.delete(arg); throw err }) // do not cache failures
cache.set(arg, p)
return p
}
}
Deep equality with cycles, dates, maps and sets:
export function deepEqual(a: unknown, b: unknown, seen = new WeakMap<object, object>()): boolean {
if (Object.is(a, b)) return true // handles NaN; distinguishes +0/-0
if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null) return false
if (Object.getPrototypeOf(a) !== Object.getPrototypeOf(b)) return false
if (seen.get(a) === b) return true // cycle already being compared
seen.set(a, b)
if (a instanceof Date) return a.getTime() === (b as Date).getTime()
if (a instanceof RegExp) return a.source === (b as RegExp).source && a.flags === (b as RegExp).flags
if (a instanceof Map) {
const bm = b as Map<unknown, unknown>
return a.size === bm.size && [...a].every(([k, v]) => bm.has(k) && deepEqual(v, bm.get(k), seen))
}
if (a instanceof Set) {
const bs = b as Set<unknown>
return a.size === bs.size && [...a].every(v => bs.has(v)) // by identity/SameValueZero, like Set itself
}
if (Array.isArray(a)) {
const ba = b as unknown[]
return a.length === ba.length && a.every((v, i) => deepEqual(v, ba[i], seen))
}
const ka = Reflect.ownKeys(a), kb = Reflect.ownKeys(b)
return ka.length === kb.length && ka.every(k => Object.prototype.hasOwnProperty.call(b, k) && deepEqual((a as any)[k], (b as any)[k], seen))
}
Deep merge that cannot be used for prototype pollution:
const isPlainObject = (v: unknown): v is Record<string, unknown> => Object.prototype.toString.call(v) === '[object Object]' && (Object.getPrototypeOf(v) === Object.prototype || Object.getPrototypeOf(v) === null)
const UNSAFE = new Set(['__proto__', 'constructor', 'prototype'])
export function deepMerge<T extends Record<string, unknown>>(target: T, ...sources: Partial<T>[]): T {
const out: Record<string, unknown> = { ...target }
for (const source of sources) {
for (const key of Object.keys(source)) {
if (UNSAFE.has(key)) continue
const value = source[key]
if (value === undefined) continue
out[key] = isPlainObject(value) && isPlainObject(out[key]) ? deepMerge(out[key] as Record<string, unknown>, value) : value // arrays replace
}
}
return out as T
}
An LRU cache and a typed event emitter:
export class LRU<K, V> {
#map = new Map<K, V>()
constructor(private readonly capacity: number) {}
get(key: K) {
if (!this.#map.has(key)) return undefined
const value = this.#map.get(key)!
this.#map.delete(key); this.#map.set(key, value) // mark as most recent
return value
}
set(key: K, value: V) {
if (this.#map.has(key)) this.#map.delete(key)
this.#map.set(key, value)
if (this.#map.size > this.capacity) this.#map.delete(this.#map.keys().next().value!) // oldest
}
}
type Listener<T> = (payload: T) => void
export function createEmitter<Events extends Record<string, unknown>>() {
const listeners = new Map<keyof Events, Set<Listener<any>>>()
return {
on<K extends keyof Events>(event: K, fn: Listener<Events[K]>) {
const set = listeners.get(event) ?? new Set()
set.add(fn); listeners.set(event, set)
return () => set.delete(fn)
},
once<K extends keyof Events>(event: K, fn: Listener<Events[K]>) {
const off = this.on(event, (p) => { off(); fn(p) })
return off
},
emit<K extends keyof Events>(event: K, payload: Events[K]) {
for (const fn of [...(listeners.get(event) ?? [])]) fn(payload) // copy: handlers may unsubscribe
},
}
}
The built-ins that replace hand-written helpers today:
const clone = structuredClone(state) // cycles, Map, Set, Date; no functions
const byStatus = Object.groupBy(orders, o => o.status) // Record<string, Order[]>
const asMap = Map.groupBy(orders, o => o.customerId)
const sorted = users.toSorted((a, b) => a.name.localeCompare(b.name)) // immutable
const both = new Set(a).intersection(new Set(b))
const firstTen = someIterable.values().filter(Boolean).take(10).toArray()
const { promise, resolve, reject } = Promise.withResolvers<string>()
await using conn = await pool.connect() // ES2026 disposal: conn[Symbol.asyncDispose]() at block end
JSON.stringify(args) as a cache key breaks on key order, undefined, Date, cycles and big inputs, and it turns two different objects with the same shape into one entry. Use identity (WeakMap) for objects and a nested Map for multiple arguments, or accept a caller-provided key function.
Iterating the live Set while a handler unsubscribes itself or another handler skips or repeats calls. Copy the set before emitting, and document whether listeners added during emit run in the same emit.
debounce with cancel, flush and a leading option; test it with Vitest fake timers, including this preservation.deepEqual and test it against cycles, NaN, dates, maps and mismatched prototypes; then compare its behaviour with structuredClone round-tripping."Each call clears and restarts the timer, so the function runs once after calls stop, with the last arguments and the original this. I expose cancel for unmount and flush to run immediately, and a leading option if the caller wants the first call to fire at once. I type it generically so the debounced function keeps its parameters. The tests, with fake timers: three calls inside the window produce one invocation with the last arguments; cancel prevents it; flush runs it immediately and clears the timer; and this is preserved. In a Vue component I would reach for VueUse's useDebounceFn, which also cancels on unmount."
"structuredClone: it handles cycles, dates, maps, sets and typed arrays, and it is native and fast. Its limits are functions, DOM nodes and class instances, which it either rejects or turns into plain objects, and it drops getters. If an interviewer wants a hand-written version, the points to hit are recursion, a WeakMap of visited objects for cycles, special-casing date, map, set and arrays, and copying only own enumerable keys."
Nothing yet. Notes filed here show up on this page and the section overview.
Coding rounds
What senior frontend coding interviews actually ask — utility functions, async orchestration, Vue component and composable drills, Nuxt fullstack drills, algorithms at frontend depth, take-home assignments and pairing sessions — with a method for each format and the habits that get scored.
Async and data processing
[object Object]