The single most useful thing to know about a JavaScript array method is whether it changes the array you called it on. Roughly half do, and mixing them up is a reliable source of bugs that only show up when an array is shared between two pieces of code.
Everything below is marked mutates or returns new.
Adding and removing
| Method | Effect | Mutates? | Returns |
|---|---|---|---|
push(x) |
Add to end | Mutates | New length |
pop() |
Remove from end | Mutates | Removed item |
unshift(x) |
Add to start | Mutates | New length |
shift() |
Remove from start | Mutates | Removed item |
splice(i, n, ...items) |
Remove/insert at index | Mutates | Array of removed items |
slice(start, end) |
Extract a section | Returns new | New array |
concat(arr) |
Join arrays | Returns new | New array |
toSpliced(i, n) |
Splice without mutating | Returns new | New array |
unshift and shift reindex every remaining element, so they are O(n). Using
shift() in a loop to drain a queue is therefore O(n²) — push to the end and
iterate forward instead.
Transforming
| Method | Effect | Mutates? |
|---|---|---|
map(fn) |
Transform each element | Returns new |
filter(fn) |
Keep elements passing a test | Returns new |
flat(depth) |
Flatten nested arrays | Returns new |
flatMap(fn) |
Map then flatten one level | Returns new |
reverse() |
Reverse order | Mutates |
toReversed() |
Reverse order | Returns new |
sort(fn) |
Sort in place | Mutates |
toSorted(fn) |
Sort | Returns new |
sort is the one that catches people most often — it mutates and returns the
same array, so this does not do what it looks like:
const sorted = original.sort() // original is now sorted too
Use [...original].sort() or original.toSorted().
The default sort is also lexicographic, not numeric:
[10, 9, 1].sort() // [1, 10, 9]
[10, 9, 1].sort((a, b) => a - b) // [1, 9, 10]
Searching
| Method | Returns | Notes |
|---|---|---|
indexOf(x) |
Index or -1 |
Uses ===, so NaN is never found |
includes(x) |
Boolean | Finds NaN, unlike indexOf |
find(fn) |
First match or undefined |
|
findIndex(fn) |
Index of first match or -1 |
|
findLast(fn) |
Last match | |
some(fn) |
Boolean — any match | Stops at the first hit |
every(fn) |
Boolean — all match | Stops at the first miss |
All of these are O(n). If you are searching the same array repeatedly, build a
Set or Map once and look up in O(1):
// O(n * m) — slow for large inputs
orders.filter(o => userIds.includes(o.userId))
// O(n + m)
const ids = new Set(userIds)
orders.filter(o => ids.has(o.userId))
Reducing
const total = items.reduce((sum, item) => sum + item.price, 0)
Always pass the initial value. Without it, reduce uses the first element as the
seed and throws on an empty array.
Do not make the reducer async — an async callback returns a promise, so the
accumulator becomes a promise on every pass:
// Wrong: sum is a Promise, not a number
items.reduce(async (acc, x) => (await acc) + x.price, 0)
Iterating
| Method | Use when |
|---|---|
for...of |
You need await, break or continue |
forEach(fn) |
Simple side effects, no early exit |
map(fn) |
You want a new array back |
entries() |
You need index and value together |
forEach cannot be stopped early and ignores the promises an async callback
returns, so this logs 0:
const out = []
;[1, 2, 3].forEach(async (n) => { out.push(await Promise.resolve(n)) })
console.log(out.length) // 0
Use for...of with await, or await Promise.all(arr.map(...)) when the work
can run concurrently.
Creating
Array.from({ length: 5 }, (_, i) => i) // [0, 1, 2, 3, 4]
Array.of(7) // [7]
new Array(7) // 7 empty slots, not [7]
[...new Set(values)] // de-duplicate
new Array(7) creating empty slots rather than a single-element array is a
long-standing wart — prefer Array.from or Array.of.
The mutation list, in one place
Memorise this and most array bugs disappear:
Mutates: push, pop, shift, unshift, splice, sort, reverse, fill, copyWithin
Returns new: map, filter, slice, concat, flat, flatMap, toSorted, toReversed, toSpliced, with