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:
// 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:
sortmutating and returning the array is what makes the bug invisible.const sorted = items.sort(...)looks pure, anditemsis now reordered too — so a component comparingprev === nextnever re-renders.
sort compares strings by default
[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
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:
// 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
items.flatMap((i) => i.tags); // one pass
items.map((i) => i.tags).flat(); // two, and an intermediate arrayflatMap 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
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
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 pointIt 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.
Related
Interview angle 7
- “Which array methods mutate?” -
push,pop,shift,unshift,splice,sort,reverse,fill.sortis the dangerous one because it mutates and returns the array, soconst sorted = items.sort(...)looks pure while reordering the original — and a framework comparing by reference sees no change. - “What are
toSortedandwith?” - the ES2023 immutable counterparts:toSorted,toReversed,toSpliced, andwith(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
reducethe wrong tool?” - when it stops reading clearly, and specifically when the accumulator is spread each iteration — that is O(n²).Object.fromEntries(map(...))orObject.groupBysay it better and run in linear time. - “
includesorindexOf?” -includesfor membership: it reads better and uses SameValueZero, so it findsNaNwhereindexOfreturns-1. UseindexOfwhen you actually want the position. - “Why is
new Array(3)different fromnew Array(3).fill(0)?” - the first is three holes, andmapskips holes so it does nothing. Methods disagree about holes —forEachskips them,mappreserves them, spread turns them intoundefined. - “How would you speed up a long
filter().map()chain?” - each step allocates a new array, so on a hot path one loop or aflatMapavoids the intermediates. Readability usually wins; knowing the reason is the point.