JavaScript Array Methods
JavaScript arrays come with a huge toolbox of built-in methods for searching, transforming, combining, and reshaping data — map(), filter(), reduce(), sort(), and dozens more. Knowing which method to reach for, and whether it changes your original array or returns a new one, is one of the highest-leverage skills in everyday JavaScript. This lesson walks through how these methods work internally, the syntax they share, and the mistakes that trip up even experienced developers.
Overview / How Array Methods Work
In JavaScript, arrays are actually specialized objects. Every array you create automatically inherits from Array.prototype, which is where all these methods — push, map, slice, find, and so on — actually live. When you write myArray.map(...), the engine looks up map on the array instance, doesn’t find it there, and walks up the prototype chain to Array.prototype.map, which then runs with myArray as its this value.
Array methods fall into two important categories, and mixing them up is a common source of bugs:
- Mutating methods change the original array in place and often return something other than a new array (like the removed element or the new length). Examples:
push,pop,shift,unshift,splice,sort,reverse,fill,copyWithin. - Non-mutating methods leave the original array untouched and return a brand-new array or value. Examples:
map,filter,slice,concat,reduce,find,includes,flat,flatMap.
Most of the powerful methods — map, filter, forEach, find, some, every, reduce — are iteration methods. They accept a callback function that the engine invokes once per element, passing three arguments: the current element, its index, and the array itself. Internally, the engine reads the array’s length once at the start of iteration, then walks indices 0 to length - 1, invoking your callback synchronously for each one (skipping empty slots in sparse arrays for most of these methods). Because the callback is just a regular function, you get full closure access to variables from the surrounding scope, which is what makes chains like filter().map().reduce() so expressive.
Syntax
Nearly every iteration method shares the same basic shape:
array.map((element, index, array) => {
return element;
});
| Part | Meaning |
|---|---|
element |
The value at the current index (required parameter). |
index |
The current position, starting at 0 (optional). |
array |
A reference to the array the method was called on (optional). |
thisArg |
An optional second argument to the method itself, used as this inside a non-arrow callback. |
Here is a quick reference of the most commonly used methods:
| Method | Mutates? | Returns |
|---|---|---|
push / pop |
Yes | New length / removed element |
shift / unshift |
Yes | Removed element / new length |
splice |
Yes | Array of removed elements |
sort / reverse |
Yes | The same array, reordered |
slice |
No | New array (shallow copy of a range) |
concat |
No | New merged array |
map |
No | New array of transformed values |
filter |
No | New array of matching values |
reduce / reduceRight |
No | A single accumulated value |
find / findIndex |
No | First matching element / its index |
some / every |
No | Boolean |
includes |
No | Boolean |
flat / flatMap |
No | New, flattened array |
Examples
Example 1: Chaining filter and map
const prices = [19.99, 5.5, 42, 3.25, 100];
const discounted = prices
.filter(price => price > 10)
.map(price => Math.round(price * 0.9 * 100) / 100);
console.log(discounted);
Output:
[ 17.99, 37.8, 90 ]
filter keeps only the prices above 10, producing a new array of three values. That new array is immediately handed to map, which applies a 10% discount and rounds to two decimals. Neither call touches the original prices array — chaining like this only works because both methods return fresh arrays.
Example 2: Reducing a cart to a total
const cart = [
{ name: 'Book', price: 12.5, qty: 2 },
{ name: 'Pen', price: 1.5, qty: 5 },
{ name: 'Notebook', price: 4, qty: 3 }
];
const total = cart.reduce((sum, item) => sum + item.price * item.qty, 0);
console.log(`Total: $${total.toFixed(2)}`);
Output:
Total: $44.50
reduce walks the array once, carrying an accumulator (sum) from one call to the next. The second argument, 0, seeds the accumulator before the first element is even processed. Each call returns sum + item.price * item.qty, which becomes the accumulator for the next iteration, until the final value — the cart total — comes out the other end.
Example 3: Sorting without mutating, and searching
const scores = [88, 42, 95, 67, 73];
const sorted = [...scores].sort((a, b) => b - a);
console.log(sorted);
console.log(scores);
const passed = scores.every(score => score >= 40);
console.log(passed);
const topScorer = scores.find(score => score > 90);
console.log(topScorer);
Output:
[ 95, 88, 73, 67, 42 ]
[ 88, 42, 95, 67, 73 ]
true
95
Spreading scores into a new array ([...scores]) before calling sort() protects the original from mutation, since sort() always reorders in place. The comparator (a, b) => b - a sorts descending. every() checks a condition against all elements and short-circuits to false the moment one fails. find() returns the first element that satisfies its test — not all of them — which is why it returns a single number, 95, rather than an array.
Under the Hood: How Iteration Methods Actually Run
When you call an iteration method like map or filter, the engine performs roughly these steps internally:
- Reads the array’s current
lengthproperty once, at the start. - Creates an index counter starting at 0.
- For each index up to that captured length, checks whether the slot actually has a value (sparse arrays have “holes” that most of these methods silently skip).
- Invokes your callback synchronously with
(element, index, array), waiting for it to return before moving to the next index. - Collects or combines the callback’s return values depending on the method:
mappushes each result into a new array,filterkeeps the element itself when the callback returns truthy,reducefeeds the return value back in as the next accumulator, andforEachsimply discards the return value entirely.
Because the length is captured up front for most methods, appending new elements to the array from inside the callback generally will not cause the method to visit them — but deleting or reassigning elements you haven’t reached yet will be reflected, since the array is read live at each index. This is exactly why mutating an array while iterating over it is risky and best avoided.
Common Mistakes
Mistake 1: Assuming sort() compares numbers numerically
By default, sort() converts every element to a string and compares them lexicographically (character by character), which produces surprising results with numbers:
const nums = [10, 1, 21, 2];
nums.sort();
console.log(nums);
Output:
[ 1, 10, 2, 21 ]
“10” sorts before “2” as a string, because “1” is less than “2” character by character. Always pass an explicit comparator for numbers:
const nums = [10, 1, 21, 2];
nums.sort((a, b) => a - b);
console.log(nums);
Output:
[ 1, 2, 10, 21 ]
Mistake 2: Forgetting to return a value inside reduce()
If your reduce callback uses a block body (curly braces) instead of an implicit arrow return, it’s easy to forget the return keyword, leaving the accumulator as undefined after the first pass:
const numbers = [1, 2, 3, 4];
const sum = numbers.reduce((acc, n) => {
acc + n;
}, 0);
console.log(sum);
Output:
undefined
The fix is to explicitly return the new accumulator value on every call:
const numbers = [1, 2, 3, 4];
const sum = numbers.reduce((acc, n) => {
return acc + n;
}, 0);
console.log(sum);
Output:
10
Mistake 3: Expecting forEach() to return a transformed array
forEach() always returns undefined, no matter what the callback returns — it exists purely for side effects, not for producing a new array:
const doubled = [1, 2, 3].forEach(n => n * 2);
console.log(doubled);
Output:
undefined
Use map() whenever you actually want a transformed array back:
const doubled = [1, 2, 3].map(n => n * 2);
console.log(doubled);
Output:
[ 2, 4, 6 ]
Best Practices
- Prefer non-mutating methods (
map,filter,slice, spread) when you can — it avoids hidden state changes that confuse other code sharing the array. - When you must use a mutating method like
sort()orsplice()but want to keep the original intact, copy first with the spread operator:[...array]. - Always pass an explicit comparator to
sort()when sorting numbers. - Always supply an initial value to
reduce()— without one, calling it on an empty array throws aTypeError, and with one element it silently skips the callback on the first item. - Use
find()orfindIndex()instead offilter(...)[0]when you only need the first match — it stops iterating as soon as it finds one, which is faster. - Use
includes()for simple existence checks andsome()/every()for condition checks, rather than manually looping. - Use
flatMap()instead ofmap().flat()when mapping each element to zero or more items — it does both in a single, more efficient pass. - Never mutate an array you’re currently iterating over with
forEach,map, orfilter— it leads to skipped or duplicated elements.
Practice Exercises
- Given
const words = ['pear', 'fig', 'banana', 'kiwi', 'watermelon'];, usefilter()andmap()to produce an array containing the uppercase version of every word with more than 4 letters. - Given
const orders = [{ amount: 25 }, { amount: 60 }, { amount: 15 }];, usereduce()to compute both the total amount and the count of orders in a single pass, returning an object like{ total: 100, count: 3 }. - Given
const nums = [3, 7, 1, 9, 4];, write one line that returnstrueif every number is less than 10, and another line that returns the first number greater than 5.
Summary
- Array methods live on
Array.prototype; all arrays inherit them through the prototype chain. - Mutating methods (
push,sort,splice, etc.) change the original array; non-mutating methods (map,filter,slice, etc.) return a new one. - Iteration methods invoke your callback with
(element, index, array)for each item, synchronously, in order. sort()compares as strings by default — always pass a comparator for numbers.reduce()needs an explicitreturnin block-bodied callbacks and should almost always get an initial value.forEach()returnsundefined; usemap()when you need a new array back.- Copy an array with
[...array]before using a mutating method if you need to preserve the original.
