Common JavaScript Array Methods You'll Use Every Day

I explain the array methods worth knowing by the three questions they answer — transform, select, reduce — plus the mutate-in-place fault line that bites everyone.

Common JavaScript Array Methods You'll Use Every Day

Quick quiz. What does this print?

[1, 30, 4, 21].sort();

Not [1, 4, 21, 30]. It prints [1, 21, 30, 4] — string order, not number order — because the default sort converts every element to a string and compares those, character by character. If that got you, you’re in good company, and it’s a symptom of how most of us learn array methods: as a grab bag of functions to memorise, each with its own fine print.

Here’s the way out of the grab bag. Almost every array method you’ll actually use answers exactly one of three questions — transform every element? keep only some? boil everything down to one value? — and the whole set is split by one fault line: which methods rewrite your original array, and which leave it alone. Get those two ideas and the methods stop being vocabulary. They become obvious.

Series: JavaScript deep-dive, part 4 — the loose opener set. Each post stands alone; start anywhere.

On this page

Three questions cover most array work

One example block, three questions, five methods. Watch what each line answers:

const prices = [19, 4, 59];

// Transform every element?
const withTax = prices.map((p) => p * 1.2); // [22.8, 4.8, 70.8]

// Keep only some?
const cheap = prices.filter((p) => p < 25); // [19, 4]

// First match? (element, not an array)
const firstCheap = prices.find((p) => p < 25); // 19

// Does anything match? / Does everything match?
prices.some((p) => p > 50); // true
prices.every((p) => p > 10); // false

Two facts about this family do most of the explaining.

They return new values and leave the original alone. map creates a new array populated with results — prices never changes above. filter and find follow the same rule: one gives you a new array of survivors, the other hands back the first matching element, or undefined when nothing matches. No surprises later in the function because nothing mutated behind your back.

The callback is a full function call, per element. Each iteration invokes your callback as its own execution — a fresh context per element, exactly the machinery from JavaScript Execution Contexts Explained. And that callback is a closure: it carries whatever variables surrounded it, which is why (p) => p * 1.2 could just as easily use a taxRate from the enclosing scope. The mechanics are covered in Understanding Closures; for array work, the takeaway is that your callback can reach outward — so keep what it reaches for small and clear.

reduce: the escape hatch

The third question is the powerful one: boil everything down to a single value. That’s reduce — it walks the array in order, passing each element plus the running result to your callback, and hands you back one final value.

const total = prices.reduce((sum, p) => sum + p, 0); // 82

That 0 on the end is the initial value, and you should pass it every time. Omit it and reduce uses the first element as the seed — which changes behaviour for short arrays and throws a TypeError on an empty one. With an initial value provided, the callback always starts at index 0, and an empty array simply returns the seed. The seed is also where reduce earns its “escape hatch” reputation: change it from a number to an object and you’re building things:

const byType = orders.reduce((groups, order) => {
  (groups[order.type] ??= []).push(order);
  return groups;
}, {});

Grouping, counting, pipelining — a huge slice of data wrangling is a reduce in a costume. The honest counterpoint: reduce is also the most over-used method in the set. If a transformation needs early exits, complex state, or reads like a puzzle, a plain for...of loop is clearer and costs nothing. Reduce is a tool for folding, not a badge to earn.

The fault line: methods that rewrite your array

Here’s where the quiz failure lives. JavaScript array methods are split into two camps, and the second camp doesn’t copy — it edits your array in place.

The big three mutators: sort, splice, reverse. Sorting happens in place, no copy is made, and the method returns a reference to your original array — which enables the classic two-in-one bug: it looks like const sorted = items.sort() gives you a copy, when sorted and items are the same array, reordered.

Method Effect on original Returns
map, filter, concat, slice untouched a new array
find, findIndex untouched element / index or undefined / -1
reduce untouched the folded value
sort, reverse, splice rewritten in place the same array
push, pop, shift, unshift mutated new length / removed element

The pair people confuse most is slice versus splice — one extra letter, opposite loyalties. slice returns a shallow copy of a portion into a new array, original untouched; splice removes or replaces elements in place. My memory hook: slice = leaves it alone; splice = performs surgery.

If you need sorted order and the original order, copy first ([...items].sort(...)) — or reach for the newer non-mutating trio toSorted, toReversed, and toSpliced, which return changed copies and leave the source intact.

And when you do sort numbers, say so with a comparator: sort((a, b) => a - b). One small guarantee while we’re here: modern JavaScript’s sort is stable — elements that compare equal keep their relative order — which matters exactly when you sort by one key after another, like sorting by grade a list already ordered by name.

Where people get burned

Three failure patterns account for most array-method bugs I see in review.

Using map for its side effects. map builds a new array; if you’re ignoring that result, you’re allocating garbage. Loop with for...of when the goal is the doing, not the returning.

Expecting find to return an array. It returns the element — one object, or undefined. Destructuring or .length on the result of find is a reliable source of undefined surprises. If you genuinely want all matches, that’s filter.

Calling .sort() on state you still need. State arrays, props, cached lists — in-place mutation through another reference is the fault line’s cruellest cut, because the bug appears far from the line that caused it. Copy first, or use the to-prefixed methods.

Try this

  1. In your browser console, run [1, 30, 4, 21].sort() and confirm you get [1, 21, 30, 4].
  2. Run [1, 30, 4, 21].sort((a, b) => a - b) and get numeric order.
  3. Assign const nums = [3, 1, 2]; const sorted = nums.sort(); then check sorted === nums.

Expected result: step 3 prints true — both names point at the same, already-mutated array. There is no copy.

Choosing without memorising

The methods choose themselves once the question is explicit:

You want Reach for
Every element transformed map
Only the elements that pass a test filter
The first element that passes find
One value folded from all of them reduce (with a seed)
Yes/no about the contents some / every
Reordered or cut copies toSorted / slice
The list itself reordered sort — knowing it mutates

None of this is thirty flashcards. It’s three questions, one fault line, and a handful of return-value facts — the sort of thing you can reconstruct at 2am instead of recall.

Learn three questions, not thirty methods

The grab-bag approach to array methods fails because it optimises for recall of functions. The model that survives real code is smaller: ask what shape comes out (new array? element? single value?), ask whether the original survives, and only then pick the name. When a sort result looks deranged, you won’t suspect the engine — you’ll remember strings. When a “copy” changes its source, you’ll know exactly which camp you invoked.

Next time you catch yourself writing a for loop over an array, pause on one line of it and ask which of the three questions it’s answering. If it’s one clean question, the method library has been waiting for you all along — and with the deep-dive fundamentals behind us, the series moves to TypeScript next, where these same callbacks start growing types.

Sources