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 length property 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: map pushes each result into a new array, filter keeps the element itself when the callback returns truthy, reduce feeds the return value back in as the next accumulator, and forEach simply 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() or splice() 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 a TypeError, and with one element it silently skips the callback on the first item.
  • Use find() or findIndex() instead of filter(...)[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 and some()/every() for condition checks, rather than manually looping.
  • Use flatMap() instead of map().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, or filter — it leads to skipped or duplicated elements.

Practice Exercises

  • Given const words = ['pear', 'fig', 'banana', 'kiwi', 'watermelon'];, use filter() and map() 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 }];, use reduce() 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 returns true if 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 explicit return in block-bodied callbacks and should almost always get an initial value.
  • forEach() returns undefined; use map() when you need a new array back.
  • Copy an array with [...array] before using a mutating method if you need to preserve the original.