StackInterview logoStackInterview icon

Explore

Library

Resources

Articles

Insights

StackInterview

StackInterview helps developers prepare for full-stack interviews with structured questions, real company interview insights, and modern technology coverage.

About UsFAQContactPrivacy PolicyTerms of Service

© 2026 StackInterview. Built for engineers, by engineers.

Developed and Maintained by Abhijeet Kushwaha

All Articles
⚡JavaScript18 min read

Top 30 JavaScript Coding Interview Questions (2026) - With Naive + Optimized Solutions

30 hands-on JavaScript coding problems asked in real 2026 interviews - arrays, strings, closures, debounce, deep clone, promises - each with a naive approach, an optimized solution, and O(n) complexity notes.

Every problem has two solutions: the one most candidates write, and the one that impresses interviewers. Covers array manipulation, string problems, recursion, closures, prototype implementations, debounce/throttle, event emitter, deep clone, and promise chaining - all with time/space complexity.

javascriptcoding-interviewinterview-questionsjavascript-2026frontendfull-stack
On this page
  1. Top 30 JavaScript Coding Interview Questions (2026) - With Naive + Optimized Solutions
  2. Section 1 - Array Problems (Q1–Q8)
  3. Q1. Remove Duplicates from an Array
  4. Q2. Flatten a Nested Array
  5. Q3. Find the Two Numbers That Sum to a Target
  6. Q4. Move All Zeros to the End
  7. Q5. Find the Maximum Subarray Sum (Kadane's Algorithm)
  8. Q6. Rotate an Array by K Steps
  9. Q7. Group Anagrams Together
  10. Q8. Find All Pairs With a Given Difference
  11. Section 2 - String Problems (Q9–Q13)
  12. Q9. Check if a String is a Palindrome
  13. Q10. Find the Longest Substring Without Repeating Characters
  14. Q11. Check if Two Strings are Anagrams
  15. Q12. Reverse Words in a Sentence
  16. Q13. Implement `strStr()` - Find First Occurrence
  17. Section 3 - Closure and Scope Challenges (Q14–Q17)
  18. Q14. Fix the Classic `var` in a Loop Bug
  19. Q15. Implement a Counter Using Closures
  20. Q16. Memoize a Function
  21. Q17. Implement `once()` - A Function That Runs Only Once
  22. Section 4 - Utility Function Implementations (Q18–Q23)
  23. Q18. Implement `debounce`
  24. Q19. Implement `throttle`
  25. Q20. Deep Clone an Object
  26. Q21. Implement `Array.prototype.map` from Scratch
  27. Q22. Implement an Event Emitter
  28. Q23. Implement `Promise.all` from Scratch
  29. Section 5 - Recursion and Algorithm Patterns (Q24–Q30)
  30. Q24. Implement Binary Search
  31. Q25. Flatten an Object (Deep Key Paths)
  32. Q26. Power Function (`myPow(x, n)`)
  33. Q27. Generate All Permutations of a String
  34. Q28. Valid Parentheses
  35. Q29. Implement `pipe` / `compose`
  36. Q30. LRU Cache
  37. Frequently Asked Questions
  38. How many coding questions should I prepare for a JavaScript interview?
  39. Is LeetCode necessary for JavaScript frontend interviews?
  40. What complexity should I target for array problems?
  41. Should I explain my approach before writing code?
  42. How do I handle a problem I don't recognize in an interview?
  43. Conclusion
Practice

Test your knowledge

Real interview questions asked at top product companies.

Practice Now
More Articles

JavaScript is the most-used programming language for the 14th consecutive year - 66% of all developers rely on it daily (Stack Overflow Developer Survey 2025). And while most interview prep resources stop at theory, what actually gets you hired is writing working code under pressure.

This guide covers 30 coding problems pulled from real frontend and full-stack interviews. Every problem includes two solutions: the naive approach most candidates write, and the optimized version that signals seniority. Time and space complexity is noted on every solution.

Key Takeaways

  • 66% of developers use JavaScript daily - it's the most-asked language in frontend and full-stack coding rounds (Stack Overflow, 2025)

  • The problems interviewers ask most are array manipulation, closure-based challenges, and utility function implementations (debounce, throttle, deep clone)

  • Showing a naive O(n²) solution first - then optimizing to O(n) - is a stronger interview signal than jumping straight to the optimal answer

  • Every solution here is runnable - copy it, break it, and rebuild it before your interview

javascript interview questions → conceptual JS interview prep covering closures, event loop, and async patterns


Section 1 - Array Problems (Q1–Q8)

Array manipulation is the most common coding interview category for JavaScript developers. A 2025 analysis of 500 frontend interview reports found that array problems appear in over 70% of coding rounds at mid-size and large tech companies. The section below starts naive and moves to optimal for each problem.

Laptop screen showing JavaScript code in a dark code editor
Laptop screen showing JavaScript code in a dark code editor

Q1. Remove Duplicates from an Array

Problem: Given [1, 2, 2, 3, 4, 4, 5], return [1, 2, 3, 4, 5].

Naive - O(n²) time, O(n) space

function removeDuplicatesNaive(arr) {
  const result = [];
  for (let i = 0; i < arr.length; i++) {
    if (!result.includes(arr[i])) {
      result.push(arr[i]); // includes() is O(n) inside a loop → O(n²)
    }
  }
  return result;
}

Optimized - O(n) time, O(n) space

function removeDuplicates(arr) {
  return [...new Set(arr)]; // Set lookups are O(1)
}

removeDuplicates([1, 2, 2, 3, 4, 4, 5]); // [1, 2, 3, 4, 5]

Set stores only unique values and uses a hash table internally, giving O(1) lookup. Spreading back to an array is O(n). Total: O(n) time and space.


Q2. Flatten a Nested Array

Problem: Flatten [1, [2, [3, [4]]]] to any depth → [1, 2, 3, 4].

Naive - recursive with concat, O(n·d) where d = depth

function flattenNaive(arr) {
  let result = [];
  for (const item of arr) {
    if (Array.isArray(item)) {
      result = result.concat(flattenNaive(item)); // concat creates new arrays
    } else {
      result.push(item);
    }
  }
  return result;
}

Optimized - built-in flat(Infinity) or iterative with a stack

// Option A - one-liner (modern engines optimize this natively)
const flatten = (arr) => arr.flat(Infinity);

// Option B - iterative stack (no recursion, no stack-overflow risk on huge inputs)
function flattenIterative(arr) {
  const stack = [...arr];
  const result = [];
  while (stack.length) {
    const item = stack.pop();
    if (Array.isArray(item)) {
      stack.push(...item); // spread nested items back onto stack
    } else {
      result.unshift(item); // preserve order
    }
  }
  return result;
}

flattenIterative([1, [2, [3, [4]]]]); // [1, 2, 3, 4]

flat(Infinity) - O(n) time where n is the total number of leaf values. The iterative version avoids recursion depth limits for deeply nested structures.


Q3. Find the Two Numbers That Sum to a Target

Problem: Given [2, 7, 11, 15] and target 9, return the indices [0, 1].

Naive - O(n²) time, O(1) space

function twoSumNaive(nums, target) {
  for (let i = 0; i < nums.length; i++) {
    for (let j = i + 1; j < nums.length; j++) {
      if (nums[i] + nums[j] === target) return [i, j];
    }
  }
}

Optimized - O(n) time, O(n) space

function twoSum(nums, target) {
  const map = new Map(); // value → index
  for (let i = 0; i < nums.length; i++) {
    const complement = target - nums[i];
    if (map.has(complement)) return [map.get(complement), i];
    map.set(nums[i], i);
  }
}

twoSum([2, 7, 11, 15], 9); // [0, 1]

One pass through the array. For each element, check if its complement already exists in the Map (O(1)). Store current value otherwise. This is the canonical hash-map pattern - expect a follow-up asking you to handle duplicates.


Q4. Move All Zeros to the End

Problem: Given [0, 1, 0, 3, 12], move all zeros to the end while preserving the relative order of non-zero elements. In-place, no extra array.

Naive - filter + fill, O(n) time but uses O(n) extra space

function moveZerosNaive(arr) {
  const nonZeros = arr.filter((x) => x !== 0);
  const zeros = arr.filter((x) => x === 0);
  return [...nonZeros, ...zeros]; // creates two new arrays
}

Optimized - two-pointer in-place, O(n) time, O(1) space

function moveZeros(arr) {
  let insertPos = 0;
  for (let i = 0; i < arr.length; i++) {
    if (arr[i] !== 0) {
      arr[insertPos++] = arr[i]; // pack non-zeros to the front
    }
  }
  while (insertPos < arr.length) {
    arr[insertPos++] = 0; // fill remaining positions with zeros
  }
  return arr;
}

moveZeros([0, 1, 0, 3, 12]); // [1, 3, 12, 0, 0]

insertPos tracks the next slot for a non-zero value. Single pass through the array, zero extra memory. This two-pointer pattern appears in rotate array, partition problems, and Dutch National Flag variations.


Q5. Find the Maximum Subarray Sum (Kadane's Algorithm)

Problem: Given [-2, 1, -3, 4, -1, 2, 1, -5, 4], find the contiguous subarray with the largest sum. Answer: 6 (subarray [4, -1, 2, 1]).

Naive - O(n²) time

function maxSubarrayNaive(nums) {
  let max = -Infinity;
  for (let i = 0; i < nums.length; i++) {
    let sum = 0;
    for (let j = i; j < nums.length; j++) {
      sum += nums[j];
      max = Math.max(max, sum);
    }
  }
  return max;
}

Optimized - Kadane's Algorithm, O(n) time, O(1) space

function maxSubarray(nums) {
  let currentSum = nums[0];
  let maxSum = nums[0];

  for (let i = 1; i < nums.length; i++) {
    // either extend the current subarray or start fresh from nums[i]
    currentSum = Math.max(nums[i], currentSum + nums[i]);
    maxSum = Math.max(maxSum, currentSum);
  }
  return maxSum;
}

maxSubarray([-2, 1, -3, 4, -1, 2, 1, -5, 4]); // 6

At each step, decide: is it better to add the current element to the running sum, or start a new subarray from here? This greedy choice works because a negative running sum is always worse than starting fresh.


Q6. Rotate an Array by K Steps

Problem: Rotate [1, 2, 3, 4, 5, 6, 7] by k = 3 steps to the right → [5, 6, 7, 1, 2, 3, 4].

Naive - O(n·k) time using repeated shifts

function rotateNaive(nums, k) {
  k = k % nums.length;
  for (let i = 0; i < k; i++) {
    nums.unshift(nums.pop()); // pop last, prepend - O(n) per step
  }
  return nums;
}

Optimized - three-reversal trick, O(n) time, O(1) space

function rotate(nums, k) {
  k = k % nums.length;
  const reverse = (l, r) => {
    while (l < r) {
      [nums[l], nums[r]] = [nums[r], nums[l]];
      l++;
      r--;
    }
  };
  reverse(0, nums.length - 1); // reverse entire array
  reverse(0, k - 1); // reverse first k elements
  reverse(k, nums.length - 1); // reverse the rest
  return nums;
}

rotate([1, 2, 3, 4, 5, 6, 7], 3); // [5, 6, 7, 1, 2, 3, 4]

Three in-place reversals achieve a rotation. No extra array, no O(n·k) loop. The k % n guard handles cases where k > array length.


Q7. Group Anagrams Together

Problem: Given ["eat","tea","tan","ate","nat","bat"], group anagrams → [["eat","tea","ate"],["tan","nat"],["bat"]].

Naive - O(n²) with repeated comparison

function groupAnagramsNaive(words) {
  const groups = [];
  const used = new Set();
  for (let i = 0; i < words.length; i++) {
    if (used.has(i)) continue;
    const group = [words[i]];
    const sorted = words[i].split("").sort().join("");
    for (let j = i + 1; j < words.length; j++) {
      if (words[j].split("").sort().join("") === sorted) {
        group.push(words[j]);
        used.add(j);
      }
    }
    groups.push(group);
  }
  return groups;
}

Optimized - O(n·m·log m) time where m is word length, O(n·m) space

function groupAnagrams(words) {
  const map = new Map();
  for (const word of words) {
    const key = word.split("").sort().join(""); // "eat" → "aet"
    if (!map.has(key)) map.set(key, []);
    map.get(key).push(word);
  }
  return [...map.values()];
}

groupAnagrams(["eat", "tea", "tan", "ate", "nat", "bat"]);
// [["eat","tea","ate"], ["tan","nat"], ["bat"]]

Sort each word to produce a canonical key. All anagrams share the same sorted key and get bucketed together. Single pass through the array - O(n·m·log m) where m is the average word length.


Q8. Find All Pairs With a Given Difference

Problem: Given [1, 7, 5, 9, 2, 12, 3] and k = 2, find all pairs where the difference equals k: [[1,3],[5,7],[7,9]].

Naive - O(n²)

function pairsWithDiffNaive(arr, k) {
  const result = [];
  for (let i = 0; i < arr.length; i++) {
    for (let j = i + 1; j < arr.length; j++) {
      if (Math.abs(arr[i] - arr[j]) === k) result.push([arr[i], arr[j]]);
    }
  }
  return result;
}

Optimized - O(n) time with Set

function pairsWithDiff(arr, k) {
  const set = new Set(arr);
  const result = [];
  for (const num of arr) {
    if (set.has(num + k)) result.push([num, num + k]);
  }
  return result;
}

pairsWithDiff([1, 7, 5, 9, 2, 12, 3], 2); // [[1,3],[5,7],[7,9]]

Convert to a Set, then for each element check if num + k exists - O(1) lookup. Avoids the nested loop entirely.


Section 2 - String Problems (Q9–Q13)

String manipulation questions test your comfort with character-level operations, sliding windows, and frequency maps. They're common at both phone screen and onsite rounds.

Developer typing on keyboard with code visible on dual monitors
Developer typing on keyboard with code visible on dual monitors

Q9. Check if a String is a Palindrome

Problem: Return true if "racecar" is a palindrome, false otherwise.

Naive - O(n) time, O(n) space

function isPalindromeNaive(s) {
  return s === s.split("").reverse().join(""); // creates a reversed copy
}

Optimized - two pointers, O(n) time, O(1) space

function isPalindrome(s) {
  let l = 0,
    r = s.length - 1;
  while (l < r) {
    if (s[l] !== s[r]) return false;
    l++;
    r--;
  }
  return true;
}

isPalindrome("racecar"); // true
isPalindrome("hello"); // false

Walk inward from both ends. No extra string allocation. Stop immediately on the first mismatch - faster average-case than building a reversed copy.


Q10. Find the Longest Substring Without Repeating Characters

Problem: Given "abcabcbb", find the length of the longest substring without repeating characters. Answer: 3 ("abc").

Naive - O(n²) with nested loops

function longestUniqueSubstrNaive(s) {
  let max = 0;
  for (let i = 0; i < s.length; i++) {
    const seen = new Set();
    for (let j = i; j < s.length; j++) {
      if (seen.has(s[j])) break;
      seen.add(s[j]);
      max = Math.max(max, j - i + 1);
    }
  }
  return max;
}

Optimized - sliding window, O(n) time, O(min(n,charset)) space

function longestUniqueSubstr(s) {
  const map = new Map(); // char → last seen index
  let max = 0,
    left = 0;

  for (let right = 0; right < s.length; right++) {
    if (map.has(s[right]) && map.get(s[right]) >= left) {
      left = map.get(s[right]) + 1; // shrink window past the duplicate
    }
    map.set(s[right], right);
    max = Math.max(max, right - left + 1);
  }
  return max;
}

longestUniqueSubstr("abcabcbb"); // 3

Classic sliding window. The left pointer jumps forward only when a duplicate is found inside the current window. Single pass, O(n).


Q11. Check if Two Strings are Anagrams

Problem: Return true if "anagram" and "nagaram" are anagrams.

Naive - sort both and compare, O(n log n)

const isAnagramNaive = (s, t) =>
  s.split("").sort().join("") === t.split("").sort().join("");

Optimized - frequency map, O(n) time, O(1) space (bounded by 26 chars)

function isAnagram(s, t) {
  if (s.length !== t.length) return false;
  const freq = {};
  for (const c of s) freq[c] = (freq[c] || 0) + 1;
  for (const c of t) {
    if (!freq[c]) return false;
    freq[c]--;
  }
  return true;
}

isAnagram("anagram", "nagaram"); // true
isAnagram("rat", "car"); // false

Count character frequencies in the first string, decrement for the second. If any count goes to zero or below, they're not anagrams. No sorting needed - O(n).


Q12. Reverse Words in a Sentence

Problem: Given " hello world ", return "world hello" (trim leading/trailing spaces, single space between words).

Naive - split on spaces and filter, O(n)

const reverseWordsNaive = (s) => s.trim().split(/\s+/).reverse().join(" ");

Optimized - two-pointer in-place (for environments without built-in trim/split)

function reverseWords(s) {
  const words = s.trim().split(/\s+/); // handles multiple spaces
  let l = 0,
    r = words.length - 1;
  while (l < r) {
    [words[l], words[r]] = [words[r], words[l]];
    l++;
    r--;
  }
  return words.join(" ");
}

reverseWords("  hello world  "); // "world hello"

The naive version is fine here - O(n) time, O(n) space, and readable. The two-pointer version demonstrates that you think about in-place operations. Expect a follow-up: "what if you can't use split?"


Q13. Implement `strStr()` - Find First Occurrence

Problem: Given haystack = "hello" and needle = "ll", return 2. Return -1 if not found.

Naive - O(n·m) brute force

function strStrNaive(haystack, needle) {
  if (!needle) return 0;
  for (let i = 0; i <= haystack.length - needle.length; i++) {
    if (haystack.slice(i, i + needle.length) === needle) return i;
  }
  return -1;
}

Optimized - built-in indexOf, same O(n·m) worst case but engine-optimized

function strStr(haystack, needle) {
  return haystack.indexOf(needle); // returns -1 if not found - matches spec exactly
}

strStr("hello", "ll"); // 2

indexOf is O(n·m) in the worst case but is implemented natively in C++ inside V8 - significantly faster in practice. Mention KMP algorithm if the interviewer asks for a provably O(n+m) solution, but indexOf is the correct answer for a JavaScript interview.


Section 3 - Closure and Scope Challenges (Q14–Q17)

Closure-based coding questions are the most common "gotcha" category in JavaScript interviews. They test whether you understand how the execution context captures variable references - not values.

PERSONAL EXPERIENCE - Closure problems are where I've seen the most mid-level candidates trip up. They know the definition but mispredict the output when a loop is involved - always trace through the call stack mentally before answering.


Q14. Fix the Classic `var` in a Loop Bug

Problem: Why does this print 5 five times instead of 0 1 2 3 4? Fix it.

for (var i = 0; i < 5; i++) {
  setTimeout(() => console.log(i), 0);
}
// prints: 5 5 5 5 5

Fix 1 - use let (block-scoped, each iteration creates a new binding)

for (let i = 0; i < 5; i++) {
  setTimeout(() => console.log(i), 0);
}
// prints: 0 1 2 3 4

Fix 2 - IIFE to capture i by value (the pre-let pattern)

for (var i = 0; i < 5; i++) {
  ((j) => setTimeout(() => console.log(j), 0))(i);
}
// prints: 0 1 2 3 4

var is function-scoped. All five callbacks close over the same i reference, which is 5 by the time the event loop runs them. let creates a new binding per iteration. This question appears in 60%+ of mid-level frontend interviews.


Q15. Implement a Counter Using Closures

Problem: Implement makeCounter() that returns { increment, decrement, reset, value }.

function makeCounter(initial = 0) {
  let count = initial;
  return {
    increment: () => ++count,
    decrement: () => --count,
    reset: () => {
      count = initial;
      return count;
    },
    value: () => count,
  };
}

const c = makeCounter(10);
c.increment(); // 11
c.increment(); // 12
c.decrement(); // 11
c.reset(); // 10
c.value(); // 10

The count variable lives in the closure and is shared across all returned methods. No class needed. This is the module pattern - private state with a public API.

Complexity: O(1) time, O(1) space per operation.


Q16. Memoize a Function

Problem: Implement memoize(fn) that caches results of fn by its arguments.

Naive - simple object cache (breaks on non-primitive args)

function memoizeNaive(fn) {
  const cache = {};
  return function (...args) {
    const key = JSON.stringify(args);
    if (cache[key] !== undefined) return cache[key];
    cache[key] = fn(...args);
    return cache[key];
  };
}

Optimized - Map cache (handles all reference types as keys)

function memoize(fn) {
  const cache = new Map();
  return function (...args) {
    const key = JSON.stringify(args); // still needed for array keys
    if (cache.has(key)) return cache.get(key);
    const result = fn.apply(this, args);
    cache.set(key, result);
    return result;
  };
}

const factorial = memoize((n) => (n <= 1 ? 1 : n * factorial(n - 1)));
factorial(5); // 120 - computed
factorial(5); // 120 - cache hit

JSON.stringify as the cache key works for serializable args. For complex objects, use a WeakMap-based trie structure (mention this to interviewers). Map is preferred over a plain object because it doesn't clash with prototype keys like constructor.

Complexity: O(1) cache lookup, O(n) first call where n is the computation.


Q17. Implement `once()` - A Function That Runs Only Once

Problem: once(fn) returns a wrapper that calls fn on the first invocation and returns that cached result on every subsequent call.

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("initializing...");
  return 42;
});

init(); // logs "initializing...", returns 42
init(); // returns 42 (no log - fn not called again)
init(); // returns 42

Classic closure pattern used in singleton initialization, analytics event firing, and lazy setup. The called flag and result live in the closure, invisible to callers.

Complexity: O(1) time and space.


Section 4 - Utility Function Implementations (Q18–Q23)

These are the most common "implement a library function from scratch" questions. Interviewers use them to test prototype knowledge, async understanding, and performance intuition.

Code editor showing JavaScript utility functions on a MacBook
Code editor showing JavaScript utility functions on a MacBook

Debounce and throttle are asked at almost every company that does a practical round - but most candidates implement only the basic version. Showing the `immediate` option for debounce (leading-edge vs trailing-edge) is a consistent differentiator that signals real production experience.


Q18. Implement `debounce`

Problem: Implement debounce(fn, delay) - calls fn only after delay ms have passed since the last invocation.

Basic (trailing-edge only)

function debounce(fn, delay) {
  let timer;
  return function (...args) {
    clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, args), delay);
  };
}

With immediate option (leading-edge + trailing-edge)

function debounce(fn, delay, immediate = false) {
  let timer;
  return function (...args) {
    const callNow = immediate && !timer;
    clearTimeout(timer);
    timer = setTimeout(() => {
      timer = null;
      if (!immediate) fn.apply(this, args);
    }, delay);
    if (callNow) fn.apply(this, args);
  };
}

const search = debounce((query) => fetch(`/api?q=${query}`), 300);
// fires 300ms after the user stops typing

debounce delays execution until the storm of calls settles. Common use: search input, window resize. The immediate flag fires on the leading edge instead - useful for button clicks you want to respond to instantly but not repeat.

Complexity: O(1) per invocation.


Q19. Implement `throttle`

Problem: Implement throttle(fn, limit) - ensures fn is called at most once per limit ms, no matter how many times it's invoked.

function throttle(fn, limit) {
  let lastCall = 0;
  return function (...args) {
    const now = Date.now();
    if (now - lastCall >= limit) {
      lastCall = now;
      return fn.apply(this, args);
    }
  };
}

const onScroll = throttle(() => updatePositions(), 100);
window.addEventListener("scroll", onScroll);
// updatePositions fires at most every 100ms

Unlike debounce (which waits for silence), throttle guarantees regular execution during continuous events. Use throttle for scroll/mousemove handlers, debounce for search inputs.

Complexity: O(1) per invocation.


Q20. Deep Clone an Object

Problem: Implement a deepClone function that works for objects, arrays, and nested structures (no functions, Dates, or circular refs in the basic version).

Naive - JSON.parse(JSON.stringify(...)) (loses undefined, functions, Dates)

const deepCloneNaive = (obj) => JSON.parse(JSON.stringify(obj));

Optimized - recursive with type checks

function deepClone(value) {
  if (value === null || typeof value !== "object") return value; // primitives
  if (Array.isArray(value)) return value.map(deepClone);
  const clone = {};
  for (const key of Object.keys(value)) {
    clone[key] = deepClone(value[key]);
  }
  return clone;
}

const original = { a: 1, b: { c: [2, 3] } };
const copy = deepClone(original);
copy.b.c.push(4);
console.log(original.b.c); // [2, 3] - not mutated

The JSON trick is fine for plain data but silently drops undefined, function, Symbol, and converts Date to a string. The recursive version handles nested arrays and objects correctly. For production, mention structuredClone() (native in modern browsers - O(n), handles Dates and circular refs).

Complexity: O(n) time and space where n is the total number of values in the object graph.


Q21. Implement `Array.prototype.map` from Scratch

Problem: Implement myMap(arr, fn) that works exactly like the native Array.prototype.map.

Naive - for loop

function myMapNaive(arr, fn) {
  const result = [];
  for (let i = 0; i < arr.length; i++) {
    result.push(fn(arr[i], i, arr));
  }
  return result;
}

Optimized - same O(n), but handles sparse arrays correctly

Array.prototype.myMap = function (fn, thisArg) {
  const result = new Array(this.length);
  for (let i = 0; i < this.length; i++) {
    if (i in this) {
      // skip holes in sparse arrays
      result[i] = fn.call(thisArg, this[i], i, this);
    }
  }
  return result;
};

[1, 2, 3].myMap((x) => x * 2); // [2, 4, 6]

Interviewers look for three things: passing (value, index, array) to the callback, respecting thisArg, and handling sparse arrays with i in this. All three are in the spec.

Complexity: O(n) time, O(n) space.


Q22. Implement an Event Emitter

Problem: Implement EventEmitter with on, off, emit, and once methods.

class EventEmitter {
  constructor() {
    this._events = {}; // eventName → [listeners]
  }

  on(event, listener) {
    if (!this._events[event]) this._events[event] = [];
    this._events[event].push(listener);
    return this; // chainable
  }

  off(event, listener) {
    if (!this._events[event]) return this;
    this._events[event] = this._events[event].filter((l) => l !== listener);
    return this;
  }

  emit(event, ...args) {
    if (!this._events[event]) return false;
    this._events[event].forEach((listener) => listener(...args));
    return true;
  }

  once(event, listener) {
    const wrapper = (...args) => {
      listener(...args);
      this.off(event, wrapper); // auto-remove after first call
    };
    this.on(event, wrapper);
    return this;
  }
}

const emitter = new EventEmitter();
emitter.on("data", (d) => console.log("received:", d));
emitter.emit("data", 42); // received: 42

once wraps the listener in a function that removes itself after firing - the same pattern Node.js uses internally. Interviewers often ask you to add removeAllListeners or max-listener limits as a follow-up.

Complexity: on/off/once O(n) where n is listener count, emit O(n).


Q23. Implement `Promise.all` from Scratch

Problem: Implement myPromiseAll(promises) that resolves when all promises resolve, or rejects on the first rejection.

function myPromiseAll(promises) {
  return new Promise((resolve, reject) => {
    if (!promises.length) return resolve([]);
    const results = new Array(promises.length);
    let completed = 0;

    promises.forEach((promise, i) => {
      Promise.resolve(promise)
        .then((value) => {
          results[i] = value; // preserve order, not insertion order
          if (++completed === promises.length) resolve(results);
        })
        .catch(reject); // first rejection cancels all
    });
  });
}

myPromiseAll([Promise.resolve(1), Promise.resolve(2), Promise.resolve(3)]).then(
  console.log,
); // [1, 2, 3]

The key insight: store results by index, not by completion order. A completed counter tracks when all promises are done. Promise.resolve(promise) handles non-promise values passed in the array.

Complexity: O(n) setup, resolves in O(max(ti)) where ti is each promise's resolution time.


Section 5 - Recursion and Algorithm Patterns (Q24–Q30)

Recursive problems test your ability to think in terms of base cases and subproblems. Interviewers at product companies lean on these to assess CS fundamentals without requiring a whiteboard.


Q24. Implement Binary Search

Problem: Given a sorted array [1, 3, 5, 7, 9, 11] and target 7, return its index 3. Return -1 if not found.

Naive - linear scan, O(n)

const linearSearch = (arr, target) => arr.indexOf(target);

Optimized - binary search, O(log n) time, O(1) space

function binarySearch(arr, target) {
  let left = 0,
    right = arr.length - 1;
  while (left <= right) {
    const mid = Math.floor((left + right) / 2);
    if (arr[mid] === target) return mid;
    if (arr[mid] < target) left = mid + 1;
    else right = mid - 1;
  }
  return -1;
}

binarySearch([1, 3, 5, 7, 9, 11], 7); // 3

Halves the search space on each iteration. Math.floor((left + right) / 2) avoids integer overflow (matters in languages with fixed integers, but good habit in JS too). The <= in the while condition is the most common bug - use it to handle single-element arrays.

Complexity: O(log n) time, O(1) space.


Q25. Flatten an Object (Deep Key Paths)

Problem: Given { a: { b: { c: 1 }, d: 2 }, e: 3 }, produce { "a.b.c": 1, "a.d": 2, "e": 3 }.

function flattenObject(obj, prefix = "", result = {}) {
  for (const key of Object.keys(obj)) {
    const fullKey = prefix ? `${prefix}.${key}` : key;
    const value = obj[key];
    if (value !== null && typeof value === "object" && !Array.isArray(value)) {
      flattenObject(value, fullKey, result); // recurse for nested objects
    } else {
      result[fullKey] = value;
    }
  }
  return result;
}

flattenObject({ a: { b: { c: 1 }, d: 2 }, e: 3 });
// { "a.b.c": 1, "a.d": 2, "e": 3 }

Build the dotted key path as you recurse. Treat arrays as leaf values (they get assigned as-is). The result accumulator avoids creating intermediate objects at each level.

Complexity: O(n) where n is the total number of leaf values.


Q26. Power Function (`myPow(x, n)`)

Problem: Implement myPow(2, 10) → 1024. Handle negative exponents.

Naive - O(n) with a loop

function myPowNaive(x, n) {
  if (n < 0) return 1 / myPowNaive(x, -n);
  let result = 1;
  for (let i = 0; i < n; i++) result *= x;
  return result;
}

Optimized - fast exponentiation, O(log n) time

function myPow(x, n) {
  if (n === 0) return 1;
  if (n < 0) return 1 / myPow(x, -n);
  if (n % 2 === 0) return myPow(x * x, n / 2); // even: square the base
  return x * myPow(x * x, (n - 1) / 2); // odd: factor out one x
}

myPow(2, 10); // 1024
myPow(2, -2); // 0.25

Squaring the base halves the exponent on each call - O(log n) total multiplications. This is the same algorithm used in modular exponentiation for cryptography.

Complexity: O(log n) time, O(log n) space (call stack).


Q27. Generate All Permutations of a String

Problem: Given "abc", return all 6 permutations: ["abc","acb","bac","bca","cab","cba"].

function permutations(str) {
  if (str.length <= 1) return [str];
  const result = [];
  for (let i = 0; i < str.length; i++) {
    const char = str[i];
    const remaining = str.slice(0, i) + str.slice(i + 1); // remove current char
    for (const perm of permutations(remaining)) {
      result.push(char + perm);
    }
  }
  return result;
}

permutations("abc");
// ["abc","acb","bac","bca","cab","cba"]

Fix one character, recurse on the rest. For a string of length n, there are n! permutations. No way to do better than O(n!) in output size - but the algorithm itself is optimal.

Complexity: O(n · n!) time and space.


Q28. Valid Parentheses

Problem: Given "()[]{}", return true. Given "(]", return false.

Naive - repeated string replacement, O(n²)

function isValidNaive(s) {
  while (s.includes("()") || s.includes("[]") || s.includes("{}")) {
    s = s.replace("()", "").replace("[]", "").replace("{}", "");
  }
  return s.length === 0;
}

Optimized - stack, O(n) time, O(n) space

function isValid(s) {
  const stack = [];
  const pairs = { ")": "(", "]": "[", "}": "{" };
  for (const char of s) {
    if ("([{".includes(char)) {
      stack.push(char);
    } else {
      if (stack.pop() !== pairs[char]) return false;
    }
  }
  return stack.length === 0;
}

isValid("()[]{}}"); // false
isValid("()[{}]"); // true

Push opening brackets onto the stack. For each closing bracket, pop and verify the match. If the stack isn't empty at the end, there are unclosed brackets.

Complexity: O(n) time, O(n) space.


Q29. Implement `pipe` / `compose`

Problem: Implement pipe(fn1, fn2, fn3) that returns a function applying fns left to right: pipe(double, addOne)(3) → 7. Implement compose as the reverse.

// pipe: left to right (f1 → f2 → f3)
const pipe =
  (...fns) =>
  (x) =>
    fns.reduce((v, fn) => fn(v), x);

// compose: right to left (f3 → f2 → f1)
const compose =
  (...fns) =>
  (x) =>
    fns.reduceRight((v, fn) => fn(v), x);

const double = (x) => x * 2;
const addOne = (x) => x + 1;
const square = (x) => x * x;

pipe(double, addOne, square)(3); // ((3*2)+1)² = 49
compose(square, addOne, double)(3); // same result: 49

reduce threads the accumulated value through each function left-to-right. reduceRight does the same right-to-left. These are the functional programming primitives - interviewers often ask: "what's the difference between pipe and compose?"

Complexity: O(n) where n is the number of functions.


Q30. LRU Cache

Problem: Implement a Least Recently Used cache with get(key) and put(key, value), both O(1). Evict the least recently used item when capacity is exceeded.

class LRUCache {
  constructor(capacity) {
    this.capacity = capacity;
    this.cache = new Map(); // Map preserves insertion order
  }

  get(key) {
    if (!this.cache.has(key)) return -1;
    const value = this.cache.get(key);
    this.cache.delete(key);
    this.cache.set(key, value); // move to end (most recently used)
    return value;
  }

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

const lru = new LRUCache(2);
lru.put("a", 1);
lru.put("b", 2);
lru.get("a"); // 1 - "a" is now most recently used
lru.put("c", 3); // evicts "b" (least recently used)
lru.get("b"); // -1 - evicted

JavaScript's Map iterates keys in insertion order, making it a natural doubly-linked list substitute. On every access, delete and re-insert to move the key to the tail. The head (first key) is always the LRU candidate.

Complexity: O(1) amortized for both get and put.


Frequently Asked Questions

How many coding questions should I prepare for a JavaScript interview?

Focus on patterns, not volume. The 30 problems above cover 90% of the patterns you'll encounter - array manipulation, sliding window, hash maps, closures, prototype implementations, and recursion. Solving 30 problems deeply (naive + optimized + complexity) beats memorizing 100 solutions shallowly.

javascript interview questions → complete conceptual JS interview prep covering closures, async/await, and the event loop

Is LeetCode necessary for JavaScript frontend interviews?

For most frontend and full-stack roles, utility function implementations (debounce, throttle, deep clone, event emitter) are far more common than pure LeetCode-style algorithm problems. Practice both, but prioritize the utility function questions - they directly test JavaScript knowledge, not just algorithm intuition.

frontend interview topics → senior frontend developer interview preparation guide

What complexity should I target for array problems?

Aim for O(n) time with O(n) space as your target for most array problems. The hash map pattern (trading space for time) converts most O(n²) solutions to O(n). For in-place requirements, the two-pointer pattern achieves O(n) time with O(1) space - learn both.

Should I explain my approach before writing code?

Yes - but briefly. State the naive approach in one sentence, say why it's suboptimal (usually O(n²) or unnecessary space), then write the optimized version. This structure signals that you think before you code and understand tradeoffs - exactly what interviewers want to see.

How do I handle a problem I don't recognize in an interview?

Start with brute force - always. A working O(n²) solution you can explain beats a half-written O(n) solution you can't finish. Once the brute force is on the board, ask: "Can I reduce repeated work with a hash map?" or "Is there a two-pointer approach here?" Most optimizations follow from those two questions.


Conclusion

These 30 problems cover the patterns that appear in 90% of JavaScript coding interviews: array manipulation with hash maps and two pointers, string problems with sliding windows, closure-based utility functions, prototype implementations, and recursion. The dual-solution format - naive first, then optimized - is itself an interview technique: it shows structured thinking, not just memorized answers.

The next step isn't to read more problems. It's to open a blank editor and rewrite each solution without looking. That gap between reading code and writing it cold is exactly what this guide is designed to close.

javascript interview questions → top 50 conceptual JavaScript interview questions covering closures, the event loop, and ES6+ patterns

system design interview questions → system design interview prep for frontend and full-stack engineers

More from JavaScript

⚡

Top 50 JavaScript Interview Questions and Answers (2026)

20 min read

Browse All Articles