JavaScript Array Methods: A Practical Guide to the 20 You Actually Use
A working reference for JavaScript array methods — which mutate, which return new arrays, and which one to reach for. With performance notes and real examples.
Last updated
Table of contents
- Mutating methods change the original array
- Non-mutating methods return something new
- Reduce is for folding, not for everything
- Searching: the four methods people mix up
- Performance: when it actually matters
- Frequently asked questions
- What is the difference between forEach and map?
- Why does my array method not work on a NodeList?
- Are array methods slower than for loops?
- How do I remove duplicates from an array?
- Related reading
- References
JavaScript has more than 30 array methods, and most of the confusion around them comes down to one question nobody answers up front: does this method change my array, or give me a new one? Get that wrong and you introduce a bug that only shows up when the same array is read twice.
This guide groups the methods by what they do to your data, not alphabetically.
Mutating methods change the original array#
These modify the array in place and return something else — usually the new length or the removed element. They are the ones that cause surprises in React state and in shared data.
const items = ['a', 'b', 'c'];
items.push('d'); // returns 4 (new length), items is now 4 long
items.pop(); // returns 'd', items is back to 3
items.splice(1, 1); // returns ['b'], items is now ['a', 'c']
items.sort(); // sorts IN PLACE and returns the same array
items.reverse(); // reverses IN PLACEsort() and reverse() are the two that catch people out, because they also return the array — so const sorted = items.sort() looks pure but has mutated items as well.
Since 2023 every modern runtime has non-mutating equivalents:
const sorted = items.toSorted(); // new array, original untouched
const reversed = items.toReversed();
const patched = items.toSpliced(1, 1);
const replaced = items.with(0, 'z'); // new array with index 0 changedNon-mutating methods return something new#
This is the group you want by default.
const numbers = [1, 2, 3, 4, 5];
numbers.map((n) => n * 2); // [2, 4, 6, 8, 10]
numbers.filter((n) => n % 2 === 0); // [2, 4]
numbers.slice(1, 3); // [2, 3]
numbers.concat([6, 7]); // [1..7]
numbers.flat(); // flattens one level
numbers.flatMap((n) => [n, n]); // map then flattenmap and filter cover the overwhelming majority of real transformations. If you find yourself writing a for loop that builds an array, one of these is almost always clearer.
Reduce is for folding, not for everything#
reduce collapses an array into a single value. It is genuinely the right tool for sums, grouping and building lookup objects:
const byId = users.reduce((accumulator, user) => {
accumulator[user.id] = user;
return accumulator;
}, {});It is the wrong tool when a simpler method exists. A reduce that returns an array is usually a map or filter in disguise, and it will be harder to read for no benefit. Note also that Object.groupBy now exists and replaces the most common grouping reduce:
const byRole = Object.groupBy(users, (user) => user.role);Searching: the four methods people mix up#
const users = [
{ id: 1, name: 'Ada' },
{ id: 2, name: 'Grace' },
];
users.find((u) => u.id === 2); // the object, or undefined
users.findIndex((u) => u.id === 2); // 1, or -1
users.some((u) => u.id === 2); // true
users.every((u) => u.id > 0); // true
users.includes(2); // false! includes uses ===That last line is the trap. includes compares with strict equality, so it works for primitives and fails silently for objects. Use some when you need a predicate.
Performance: when it actually matters#
For arrays under a few thousand elements, method choice makes no measurable difference — write the clearest version. Two cases where it does matter:
- Chaining creates intermediate arrays.
data.filter(...).map(...).filter(...)allocates three arrays. On a 100,000-element list in a hot path, a single loop or a generator is meaningfully faster. includesinside a loop is O(n²). Checking membership against an array inside another array's iteration is the classic accidental quadratic. Build aSetfirst: lookup goes from O(n) to O(1).
const allowed = new Set(allowedIds); // once
const visible = items.filter((i) => allowed.has(i.id)); // O(n) totalFrequently asked questions#
What is the difference between forEach and map?#
forEach returns undefined and exists purely for side effects. map returns a new array of the same length. If you are not using the return value, forEach communicates that; if you are, you wanted map. A forEach that pushes into an outer array is a map written the long way.
Why does my array method not work on a NodeList?#
Because document.querySelectorAll returns a NodeList, not an array. It has forEach but not map or filter. Convert it first with Array.from(nodes) or [...nodes].
Are array methods slower than for loops?#
Marginally, because of the function call per element — but the difference is in the low single-digit nanoseconds and is dwarfed by anything you do inside the callback. Optimise for readability first and measure before rewriting.
How do I remove duplicates from an array?#
[...new Set(items)] for primitives. For objects you need a key: build a Map keyed by the identifying field and take [...map.values()], since two objects with identical contents are never === to each other.
Related reading#
- JavaScript's Three Equality Operators — why
includesbehaves that way - Async/Await Explained — for
Promise.allover mapped arrays - Need to inspect a nested array quickly? The JSON Formatter shows structure at a glance.