JavaScript Array Interview Questions
Short answer
Array questions test one thing above all: whether you know which methods mutate. Everything else - reduce, the sort comparator, deduping with a Set, find versus filter, follows from understanding that a copy and an in-place change are different operations with different consequences in React.
| Mutates in place | Returns a new array | Returns something else |
|---|---|---|
| push, pop | map, filter | reduce → any value |
| shift, unshift | slice, concat | find → element or undefined |
| splice | flat, flatMap | findIndex → number |
| sort, reverse | toSorted, toReversed | some, every, includes → boolean |
| fill, copyWithin | toSpliced, with | join → string |
What array questions are really testing
On the surface these are vocabulary questions, name the difference between map and forEach, explain reduce. Underneath, almost every one is checking whether you distinguish an operation that returns a new array from one that changes the array you were given. That distinction is the entire reason React state updates break, why a sorted prop mutates the parent's data, and why a shared default array leaks between calls.
So the answers that score attach the method to that consequence. "filter returns a new array" is a definition. "filter returns a new array, which is why it is safe in a React setState and why sort is not" is the same fact with the reason someone asked for it. Deeper head-to-head comparisons live on their own pages; here the goal is to hold the whole set in your head as one system.
map, forEach or filter, which one and why?
`map` transforms every element and returns a new array of the same length. `filter` keeps the elements that pass a test and returns a shorter one. `forEach` returns undefined and exists purely for side effects. The rule is simple: if you want a value back, do not use forEach; if you are producing the same number of items, use map.
1const nums = [1, 2, 3, 4];
2
3console.log(nums.map((n) => n * 2)); // → [ 2, 4, 6, 8 ]
4console.log(nums.filter((n) => n % 2 === 0)); // → [ 2, 4 ]
5console.log(nums.forEach((n) => n * 2)); // → undefined
6
7// Mistake 1: map used purely for a side effect
8nums.map((n) => console.log(n));
9// → works, but builds a throwaway array of undefined. forEach says the intent.
10
11// Mistake 2: forgetting the return in a block body
12const bad = nums.map((n) => { n * 2; });
13console.log(bad); // → [ undefined, undefined, undefined, undefined ]
14
15// map + filter in one pass, when the transform can reject
16console.log(nums.flatMap((n) => (n % 2 ? [] : [n * 10])));
17// → [ 20, 40 ] empty array removes the element
18
19// Chaining is fine and readable; each step allocates
20console.log(nums.filter((n) => n > 1).map((n) => n * n).join("-"));
21// → 4-9-16The follow-up worth pre-empting is whether chaining is wasteful, because it does allocate an intermediate array at each step. The honest answer is that for the array sizes a frontend deals with, a few hundred rows, the readability is worth far more than the allocation, and that `flatMap` or a single `reduce` is the answer if a profile ever says otherwise. Claiming the chain is a performance problem without measuring it is the wrong answer in both directions.
How does reduce actually work?
`reduce` folds an array into a single value by carrying an accumulator across every element. It is the most general of the array methods, map and filter can both be written with it, and interviewers like it because the answer reveals whether you have used it for anything beyond summing numbers.
1const orders = [
2 { kit: "js-kit", amount: 299 },
3 { kit: "react-kit", amount: 449 },
4 { kit: "js-kit", amount: 299 },
5];
6
7// The accumulator can be any type, here a number, then an object
8console.log(orders.reduce((sum, o) => sum + o.amount, 0));
9// → 1047
10
11console.log(
12 orders.reduce((acc, o) => {
13 (acc[o.kit] ??= []).push(o.amount);
14 return acc;
15 }, {})
16);
17// → { 'js-kit': [ 299, 299 ], 'react-kit': [ 449 ] }
18
19// Without an initial value, the FIRST ELEMENT becomes the accumulator
20console.log([1, 2, 3].reduce((a, b) => a + b));
21// → 6 happens to work
22
23console.log([].reduce((a, b) => a + b, 0));
24// → 0 safe
25
26// [].reduce((a, b) => a + b);
27// → TypeError: Reduce of empty array with no initial value
28
29// The classic bug: forgetting to return the accumulator
30// orders.reduce((acc, o) => { acc[o.kit] = o.amount; }, {});
31// → TypeError: Cannot set properties of undefinedTwo things to say without being asked. Always pass the initial value, it fixes the accumulator's type, it makes the empty-array case safe, and the version that throws only throws on the empty input, so it survives every test with data in it. And be willing to say that a `for...of` loop is clearer than a clever reduce; interviewers who like reduce also like candidates who know when not to use it.
Which array methods mutate, and why does it matter?
`push`, `pop`, `shift`, `unshift`, `splice`, `sort`, `reverse` and `fill` change the array in place. Everything else returns a new one. In React this is not a style preference: state updates are detected by reference comparison, so mutating an array and setting the same reference back produces no re-render at all.
1const items = ["a", "b", "c"];
2
3// Mutation - same reference, so React sees no change
4const wrong = items;
5wrong.push("d");
6console.log(wrong === items); // → true setState would do nothing
7
8// Immutable equivalents
9console.log([...items, "d"]); // push
10console.log(items.slice(0, -1)); // pop
11console.log(["z", ...items]); // unshift
12console.log(items.filter((x) => x !== "b")); // remove by value
13console.log(items.toSpliced(1, 1)); // remove by index, ES2023
14console.log(items.with(1, "B")); // replace by index, ES2023
15// → [ 'a', 'B', 'c' ]
16
17// Updating one object inside an array
18const rows = [{ id: 1, done: false }, { id: 2, done: false }];
19console.log(rows.map((r) => (r.id === 2 ? { ...r, done: true } : r)));
20// → [ { id: 1, done: false }, { id: 2, done: true } ]
21// → row 1 keeps its identity, so a memoised child does not re-renderThe last example is the one to show if you only show one. Mapping and spreading only the row that changed means every other object keeps its reference, so `React.memo` children that received them do not re-render. Rebuilding all the rows with a deep clone produces the same visible result and quietly makes the list re-render in full, which is exactly the kind of thing a performance-focused interviewer is probing for.
Why does [1, 10, 9].sort() come out wrong?
Because the default comparator converts every element to a string, so "10" sorts before "9" for the same reason "apple" precedes "banana". It is a favourite question because the array in the example looks almost sorted, so the bug reads as intermittent rather than systematic.
1console.log([1, 10, 9, 2].sort());
2// → [ 1, 10, 2, 9 ] string comparison
3
4console.log([1, 10, 9, 2].sort((a, b) => a - b));
5// → [ 1, 2, 9, 10 ]
6
7// sort mutates AND returns the same array
8const arr = [3, 1, 2];
9console.log(arr.sort((a, b) => a - b) === arr); // → true
10
11// Non-mutating options
12console.log([...arr].sort((a, b) => b - a)); // → [ 3, 2, 1 ]
13console.log([3, 1, 2].toSorted((a, b) => a - b)); // → [ 1, 2, 3 ] ES2023
14
15// Text: localeCompare, not < and >
16console.log(["Z", "a", "B"].sort());
17// → [ 'B', 'Z', 'a' ] uppercase first, by code unit
18console.log(["Z", "a", "B"].sort((x, y) => x.localeCompare(y)));
19// → [ 'a', 'B', 'Z' ]
20
21// Sorting by a field, with a tiebreak
22const users = [{ n: "b", age: 30 }, { n: "a", age: 30 }];
23console.log(users.sort((x, y) => y.age - x.age || x.n.localeCompare(y.n)));
24// → [ { n: 'a', age: 30 }, { n: 'b', age: 30 } ]The `||` tiebreak pattern is worth showing because it comes up in every real table implementation and because it demonstrates that a comparator is just a number: negative, zero or positive. And naming that `sort` is stable since ES2019, equal elements keep their original relative order, is a detail that lands well, since it is what makes chained sorts predictable.
This is 1 of 80+ questions in the JavaScript Interview Kit
Get every question with detailed answers, follow-ups and real code, plus coding challenges and a last-minute revision sheet. One-time payment, instant access.
⚡ Get the JavaScript Interview Kit → ₹299How do you remove duplicates from an array?
For primitives, wrap it in a `Set` and spread back. For objects it is more interesting, because a Set compares by reference and two objects with identical contents are different values, so you have to choose a key and dedupe on that.
1console.log([...new Set([1, 2, 2, 3, 1])]);
2// → [ 1, 2, 3 ] insertion order preserved
3
4// Objects: a Set does nothing, because these are different references
5const a = { id: 1 }, b = { id: 1 };
6console.log([...new Set([a, b])].length);
7// → 2
8
9// Dedupe by key - a Map keeps the LAST occurrence
10const rows = [{ id: 1, v: "old" }, { id: 2, v: "x" }, { id: 1, v: "new" }];
11console.log([...new Map(rows.map((r) => [r.id, r])).values()]);
12// → [ { id: 1, v: 'new' }, { id: 2, v: 'x' } ]
13
14// Keep the FIRST occurrence instead
15const seen = new Set();
16console.log(rows.filter((r) => !seen.has(r.id) && seen.add(r.id)));
17// → [ { id: 1, v: 'old' }, { id: 2, v: 'x' } ]
18
19// The O(n squared) version an interviewer will ask you to improve
20console.log(rows.filter((r, i, all) => all.findIndex((x) => x.id === r.id) === i));
21// → same result, but findIndex rescans the array for every elementThe last two lines are the actual question in most interviews. The `findIndex` one-liner is what people write first and it is quadratic, because it scans the whole array for every element; the Set and Map versions are linear because a lookup is constant time. Being able to say "that is O(n squared), here is the O(n) version" without being prompted is the whole point of asking.
find, some, every or includes?
Four methods that all search, differing in what they return and when they stop. `find` returns the first matching element or undefined; `findIndex` returns its position or -1; `some` and `every` return booleans; `includes` tests for a specific value. All of them short-circuit, which `filter` does not.
1const nums = [0, 5, 8];
2
3console.log(nums.find((n) => n > 4)); // → 5
4console.log(nums.find((n) => n > 99)); // → undefined
5console.log(nums.findIndex((n) => n > 99)); // → -1
6console.log(nums.some((n) => n > 4)); // → true
7console.log(nums.every((n) => n > 4)); // → false
8console.log(nums.includes(8)); // → true
9console.log(nums.findLast((n) => n > 4)); // → 8 ES2023
10
11// The trap: find can legitimately return a falsy element
12const zero = nums.find((n) => n === 0);
13if (zero) console.log("found");
14// → nothing printed! 0 is falsy, so the guard is wrong
15if (zero !== undefined) console.log("found");
16// → found
17
18// every on an empty array is true, vacuous truth, and a real bug source
19console.log([].every((n) => n > 100)); // → true
20console.log([].some((n) => n > 100)); // → falseBoth traps here are worth knowing. A truthiness check on a `find` result breaks whenever the array can contain 0, an empty string or false, so compare against undefined instead. And `every` on an empty array returns true, which means a validation written as "every field is valid" silently passes when there are no fields, a bug that survives most test suites.
What are sparse arrays and array holes?
An array with gaps - created by `new Array(3)`, by deleting an element, or by assigning past the end. Holes are not `undefined`; they are the absence of a property, and most array methods skip them, which produces behaviour that looks impossible until you know what is happening.
1const holes = new Array(3);
2console.log(holes.length); // → 3
3console.log(holes); // → [ <3 empty items> ]
4
5console.log(holes.map((_, i) => i));
6// → [ <3 empty items> ] map SKIPS holes, so the callback never ran
7
8// The fixes
9console.log(Array.from({ length: 3 }, (_, i) => i)); // → [ 0, 1, 2 ]
10console.log([...new Array(3)].map((_, i) => i)); // → [ 0, 1, 2 ]
11console.log(new Array(3).fill(0).map((_, i) => i)); // → [ 0, 1, 2 ]
12
13// delete leaves a hole; splice does not
14const xs = [1, 2, 3];
15delete xs[1];
16console.log(xs, xs.length); // → [ 1, <1 empty item>, 3 ] 3
17
18const ys = [1, 2, 3];
19ys.splice(1, 1);
20console.log(ys, ys.length); // → [ 1, 3 ] 2
21
22// Which methods see holes is inconsistent, another reason to avoid them
23console.log(Object.keys(new Array(3))); // → []
24console.log(new Array(3).includes(undefined)); // → true`Array.from({ length: n }, fn)` is the answer to remember, because it never creates holes in the first place. The wider point, and the one that makes this a good answer rather than trivia, is that `delete` on an array is almost always a bug: it leaves the length unchanged and puts a hole in the middle. `splice`, `filter` or `toSpliced` are what you actually want.
How do you turn an array-like into a real array?
An array-like has a `length` and numeric keys but no array methods, `arguments`, a NodeList from `querySelectorAll`, a string. The question is really about knowing that `Array.from` and spreading are not interchangeable: spreading needs the value to be iterable, and `Array.from` only needs it to have a length.
1function args() {
2 // arguments.map(...) // → TypeError: not a function
3 return Array.from(arguments).map((x) => x * 2);
4}
5console.log(args(1, 2, 3)); // → [ 2, 4, 6 ]
6
7// A NodeList is iterable, so both work
8// const nodes = [...document.querySelectorAll("li")];
9// const also = Array.from(document.querySelectorAll("li"));
10
11// A plain array-like is NOT iterable, only Array.from works
12const like = { 0: "a", 1: "b", length: 2 };
13console.log(Array.from(like)); // → [ 'a', 'b' ]
14// console.log([...like]); // → TypeError: like is not iterable
15
16// Array.from takes a map function, saving an allocation
17console.log(Array.from("abc", (c) => c.toUpperCase()));
18// → [ 'A', 'B', 'C' ]
19
20// Sets and Maps are iterable, so spread works on those
21console.log([...new Set([1, 1, 2])]); // → [ 1, 2 ]Array, Set or Map, when do you reach for each?
This is the follow-up to the dedupe question, and it is where an array round turns into a data-structures round. An array is an ordered list you index by position; its lookups are linear, because finding a value means scanning until you hit it. A `Set` is an unordered-by-key collection of unique values with constant-time membership tests. A `Map` is a set of key-value pairs where the keys can be any type, including objects, and are compared by reference rather than stringified.
The decision rule that answers the question cleanly: use an array when order matters or you will iterate the whole thing anyway; switch to a Set the moment you are calling `includes` inside a loop, because that is an O(n squared) pattern hiding in readable code; and use a Map when the keys are objects, when you need to preserve key insertion order reliably, or when you will be adding and deleting entries frequently. A plain object is still the right choice for a fixed, known set of string keys, a config, a props bag, where the shape is part of the code rather than the data.
1const allowed = ["js-kit", "react-kit", "complete-frontend-kit"];
2const orders = [{ kit: "js-kit" }, { kit: "unknown-kit" }];
3
4// O(n * m) - includes rescans the array for every order
5console.log(orders.filter((o) => allowed.includes(o.kit)).length);
6// → 1 correct, but the cost grows with BOTH lists
7
8// O(n) - build the Set once, then every lookup is constant time
9const allowedSet = new Set(allowed);
10console.log(orders.filter((o) => allowedSet.has(o.kit)).length);
11// → 1
12
13// Map keeps objects as keys; a plain object cannot
14const meta = new Map([[orders[0], { paid: true }]]);
15console.log(meta.get(orders[0]).paid); // → true
16console.log(Object.keys({ [orders[0]]: 1 }));
17// → [ '[object Object]' ] the object collapsed to a stringThe reason interviewers like this thread is that the array version is not wrong, it produces the right answer and reads perfectly well, so noticing the complexity is a genuine signal rather than a memorised fact. Saying "that is fine for three allowed kits and I would build a Set once the list came from the database" is a better answer than either extreme, because it names the trade-off instead of applying a rule blindly.
What loses marks in an array round
Everyone knows these methods, so the differentiation comes entirely from precision.
- →Not knowing which methods mutate, the one fact the whole topic rests on.
- →Using map for a side effect, or forEach and then wondering where the result went.
- →Omitting reduce's initial value, then being unable to say what breaks.
- →Calling sort() on numbers and not spotting the string comparison.
- →Writing an O(n squared) dedupe and not recognising it as one.
- →A truthiness check on a find result that can legitimately be 0 or an empty string.
- →delete on an array element, leaving a hole and an unchanged length.
Frequently asked questions
- What is the difference between map and forEach?
- map runs the callback on every element and returns a new array of the results, so it is chainable. forEach runs the callback and returns undefined, so it exists only for side effects. If you want a value back, use map; if you are just doing something per item, forEach states that intent clearly.
- Which JavaScript array methods mutate the original array?
- push, pop, shift, unshift, splice, sort, reverse, fill and copyWithin all mutate in place. map, filter, slice, concat, flat, flatMap and the ES2023 methods toSorted, toReversed, toSpliced and with all return a new array and leave the original untouched.
- How does reduce work in JavaScript?
- It folds an array into a single value by carrying an accumulator through every element. The callback receives the accumulator and the current element and must return the next accumulator. Always pass an initial value: it fixes the accumulator's type and stops an empty array throwing a TypeError.
- Why does [1, 10, 9].sort() return the wrong order?
- The default comparator converts elements to strings, so "10" sorts before "9". Pass a numeric comparator - sort((a, b) => a - b). Also remember that sort mutates the array and returns the same reference, so copy first with spread or use toSorted when the original must not change.
- How do I remove duplicates from an array in JavaScript?
- For primitives, [...new Set(arr)] in one line, preserving insertion order. For objects, a Set will not help because it compares by reference, dedupe on a key with [...new Map(arr.map(x => [x.id, x])).values()], which keeps the last occurrence, or filter with a seen Set to keep the first.
- What is the difference between find and filter?
- find returns the first matching element or undefined and stops as soon as it matches. filter returns an array of every match and always scans the whole array. Use find when you expect one result, filter(...)[0] does more work and reads as though several matches were expected.
- What is the difference between slice and splice?
- slice returns a shallow copy of a portion and never changes the original. splice mutates the array, removing and optionally inserting elements, and returns the removed ones. The names are close enough that the mutation is easy to forget, which is exactly why the question is asked.
- Why does new Array(3).map() not work?
- new Array(3) creates a sparse array of three holes rather than three undefined values, and map skips holes entirely, so the callback never runs. Use Array.from({ length: 3 }, (_, i) => i), spread it first, or call fill() before map.
- What is an array-like object?
- An object with a length property and numeric keys but no array methods, arguments, a NodeList, a string. Convert it with Array.from, which needs only a length. Spreading also works but requires the value to be iterable, so it fails on a plain array-like object.
- Is chaining map and filter bad for performance?
- Rarely. Each step allocates an intermediate array, but for the few hundred items a frontend typically handles the readability is worth far more than the allocation. If a profile proves otherwise, combine the steps into a single reduce or use flatMap. Optimising this without measuring is the more common mistake.
- Is Array.prototype.sort stable in JavaScript?
- Yes, since ES2019 the specification requires it, so elements that compare as equal keep their original relative order in every modern engine. That is what makes sorting by one field and then another produce predictable results, and it is worth mentioning when a comparator question comes up.
- Why should I never use delete on an array element?
- delete removes the property but leaves the length unchanged, so you get a hole in the middle that most methods skip and some treat as undefined. The visible result is an array that reports a length larger than the number of items it has. Use splice, filter or toSpliced instead. If an interviewer asks why length stays the same, the answer is that delete only removes own properties and never touches the array's length bookkeeping.
This is 1 of 80+ questions in the JavaScript Interview Kit
Get every question with detailed answers, follow-ups and real code, plus coding challenges and a last-minute revision sheet. One-time payment, instant access.
⚡ Get the JavaScript Interview Kit → ₹299