Frontend / JavaScript core / array-methods.md

Array methods

Updated 7 interview angles 5 min read source
On this page9
  1. Mutating or not — the table that matters
  2. sort compares strings by default
  3. reduce, and when not to reach for it
  4. Finding things
  5. Flattening and chaining
  6. Sparse arrays behave inconsistently
  7. Array.from is the general converter
  8. Related
  9. Interview angle

Array methods

Nobody is tested on map. What gets probed is which methods mutate, what sort does by default, and how the newer immutable variants change the advice you were given five years ago.

Mutating or not — the table that matters

Mutates Returns a copy
push pop shift unshift concat slice map filter
splice sort reverse fill toSpliced toSorted toReversed with

The right-hand column’s second row is ES2023 and it exists because the left-hand column is a permanent hazard in React, Vue and anything else that compares by reference:

js
// Mutates in place, returns the SAME array. React sees no change.
const sorted = items.sort((a, b) => a.n - b.n);
sorted === items;              // true

// ES2023: leaves `items` alone.
const sorted = items.toSorted((a, b) => a.n - b.n);

toSorted, toReversed, toSpliced and with are the immutable counterparts. with(i, v) replaces one index and returns a copy, which replaces the [...arr.slice(0, i), v, ...arr.slice(i + 1)] dance entirely.

Gotcha: sort mutating and returning the array is what makes the bug invisible. const sorted = items.sort(...) looks pure, and items is now reordered too — so a component comparing prev === next never re-renders.

sort compares strings by default

js
[10, 9, 1].sort();                  // [1, 10, 9]   — lexicographic
[10, 9, 1].sort((a, b) => a - b);   // [1, 9, 10]

Without a comparator, elements are converted to strings. This is the most frequently demonstrated JavaScript surprise, and the fix is always to pass a comparator.

The comparator must be consistent — return a negative, zero or positive number, and give the same answer for the same pair every time. Returning a > b (a boolean) works by accident in some engines and produces garbage in others.

Sort is stable as of ES2019, so equal elements keep their relative order, which is what makes multi-key sorting work by sorting twice.

reduce, and when not to reach for it

js
const total = items.reduce((sum, i) => sum + i.price, 0);

// Grouping — but Object.groupBy (ES2024) says it better.
const byKind = Object.groupBy(items, (i) => i.kind);

Two rules. Always pass the initial value: reduce on an empty array with no seed throws TypeError, and with a seed it returns the seed. And stop when it stops reading well — a reduce building an object with a spread inside is O(n²) and harder to read than a loop:

js
// O(n²): a new object per iteration.
items.reduce((acc, i) => ({ ...acc, [i.id]: i }), {});

// O(n), and clearer.
Object.fromEntries(items.map((i) => [i.id, i]));

Finding things

Method Returns Uses
indexOf index or -1 strict equality
includes boolean SameValueZero — finds NaN
find the element a predicate
findIndex index or -1 a predicate
findLast the element, from the end a predicate
some / every boolean short-circuit

includes over indexOf for a membership test, because [NaN].indexOf(NaN) is -1 and [NaN].includes(NaN) is true. And indexOf(x) !== -1 reads worse than includes(x).

some and every short-circuit; filter(...).length > 0 walks the whole array and allocates. On a large array that is the difference between the first match and all of them.

Flattening and chaining

js
items.flatMap((i) => i.tags);          // one pass
items.map((i) => i.tags).flat();       // two, and an intermediate array

flatMap flattens exactly one level and is the idiom for “map, where some inputs produce zero or several outputs” — returning [] from the callback drops the element.

Chaining filter().map() allocates an array per step. Readability usually wins, but on a hot path over a large array one loop is measurably better, and knowing why — the intermediate allocations — is the point.

Sparse arrays behave inconsistently

js
const a = [1, , 3];         // hole at index 1
a.length;                   // 3
a.forEach((x) => x);        // callback runs twice — holes skipped
a.map((x) => 1);            // [1, <hole>, 1] — hole preserved
[...a];                     // [1, undefined, 3] — hole becomes undefined
Array.from(a);              // [1, undefined, 3]

Different methods disagree about whether a hole exists. That is why new Array(3).fill(0) is the idiom rather than new Array(3) — the latter is all holes and map over it does nothing.

Array.from is the general converter

js
Array.from({ length: 5 }, (_, i) => i * i);   // [0, 1, 4, 9, 16]
Array.from(document.querySelectorAll("li"));  // NodeList -> real Array
Array.from(new Set(items));                   // dedupe
Array.from("héllo");                          // splits by code point

It takes anything iterable or array-like plus an optional map function, so it replaces the old Array.prototype.slice.call(arguments) trick. The last line is the one worth remembering: "héllo".split("") breaks surrogate pairs and Array.from does not.

Interview angle 7

  • “Which array methods mutate?” - push, pop, shift, unshift, splice, sort, reverse, fill. sort is the dangerous one because it mutates and returns the array, so const sorted = items.sort(...) looks pure while reordering the original — and a framework comparing by reference sees no change.
  • “What are toSorted and with?” - the ES2023 immutable counterparts: toSorted, toReversed, toSpliced, and with(i, v) for replacing one index. They exist precisely because the mutating versions are a hazard in reactive frameworks.
  • “Why does [10, 9, 1].sort() give [1, 10, 9]?” - without a comparator the elements are converted to strings and compared lexicographically. Always pass one, and make it return a number rather than a boolean.
  • “When is reduce the wrong tool?” - when it stops reading clearly, and specifically when the accumulator is spread each iteration — that is O(n²). Object.fromEntries(map(...)) or Object.groupBy say it better and run in linear time.
  • includes or indexOf?” - includes for membership: it reads better and uses SameValueZero, so it finds NaN where indexOf returns -1. Use indexOf when you actually want the position.
  • “Why is new Array(3) different from new Array(3).fill(0)?” - the first is three holes, and map skips holes so it does nothing. Methods disagree about holes — forEach skips them, map preserves them, spread turns them into undefined.
  • “How would you speed up a long filter().map() chain?” - each step allocates a new array, so on a hot path one loop or a flatMap avoids the intermediates. Readability usually wins; knowing the reason is the point.