Array methods

Choose array methods by return value and mutation behavior, without getting caught by sorting, sparse arrays, shallow copies, or async callbacks.

level beginner time 9 min at Standard depth
version Node 24
what

Array methods express common loops such as transformation, filtering, search, reduction, and reordering, each with a defined return contract.

trap

Similar-looking methods have different side effects: sort() and splice() mutate the array, while toSorted() and toSpliced() return shallow copies.

fix

Decide the result shape first, then check mutation, empty inputs, empty slots, and whether anything actually awaits the callback.

What it is and why it exists

A JavaScript array is an object that organizes values under integer indexes, with length recording the index range. Array methods are operations on Array.prototype that express intentions such as transforming each item, retaining matching items, finding one match, reducing items to one result, or changing their order.

These methods make recurring loops easier to read. A hand-written for loop can do the same work, but readers have to infer the goal from its counter, conditions, and accumulator; map(), filter(), find(), and reduce() state the result shape directly.

Array methods don’t follow one mutation policy. push(), splice(), sort(), and reverse() mutate the receiver, while map(), filter(), slice(), and newer methods such as toSorted() create arrays. Choosing the wrong policy can damage an array that another caller still uses even when the immediate output looks correct.

You’ll meet these methods in API data cleanup, UI list updates, validation, search, and aggregation. Choose one from the required result, permitted side effects, and boundary inputs instead of trying to memorize the whole API.

How it works

Classify a method by what it returns and whether it changes the original array. The table covers the main contracts; “new array” means only the outer array is new, not that its element objects were deeply copied.

GoalCommon methodsResultMutates original
Perform one side effect per itemforEach()undefinedNo
Transform or filter itemsmap(), filter(), flatMap()New arrayNo
Find an item or indexfind(), findLast(), findIndex()Item, index, or not-found markerNo
Test a conditionsome(), every(), includes()BooleanNo
Aggregatereduce(), reduceRight()Any accumulated resultCallback-dependent
Extract or combineslice(), concat(), flat()New arrayNo
Add, remove, or reorder in placepush(), pop(), splice(), sort(), reverse()Method-dependentYes
Add, remove, or reorder a copytoSpliced(), toSorted(), toReversed(), with()New arrayNo

Iterative methods such as map(), filter(), find(), some(), and every() take a callback. The callback usually receives the current value, its index, and the original array; an arrow function can omit parameters it doesn’t need. A reduce() callback instead receives the accumulator, current value, index, and original array.

The callback’s return value has a different meaning for each method. map() stores it, filter() uses its truthiness to retain the original item, find() returns the first matching item, and some() or every() stops once the answer is known. forEach() ignores callback return values, so it can’t build a result or end iteration early.

Copying methods make a shallow copy . The outer array has a new identity, but objects, arrays, and functions inside it remain the original references. Changing the new array’s length or slots doesn’t affect the old array; changing a nested object through a shared element is visible from both.

Sorting also needs a comparator . A negative result puts a before b, a positive result puts a after b, and 0 leaves them equal in order. A comparator that returns only a Boolean can’t correctly express all three outcomes.

Examples

Deriving new results from records

Start with the common case: a pipeline without side effects. Filtering paid orders, formatting labels, and totaling them produce different results, so they use filter(), map(), and reduce() respectively; short-circuiting checks belong to some() and every().

order-summary.js
const orders = [
  { id: 'A-101', status: 'paid', total: 48 },
  { id: 'A-102', status: 'pending', total: 125 },
  { id: 'A-103', status: 'paid', total: 80 },
];

const paidOrders = orders.filter((order) => order.status === 'paid');
const labels = paidOrders.map(
  (order) => `${order.id}: EUR ${order.total}`,
);
const paidTotal = paidOrders.reduce(
  (sum, order) => sum + order.total,
  0,
);

console.log(labels);
console.log(paidTotal);
console.log(orders.some((order) => order.total >= 100));
console.log(orders.every((order) => order.id.startsWith('A-')));
[ 'A-101: EUR 48', 'A-103: EUR 80' ]
128
true
true

None of the three array methods changes orders. The 0 passed to reduce() makes an empty set of paid orders produce the number 0, and it establishes the accumulator type before the first callback.

The example doesn’t force everything into one reduce() call. When an intermediate result such as paidOrders has a clear business meaning, keeping it is often easier to inspect than mixing filtering, formatting, and totaling into one callback.

Copying updates and shared elements

Use the copying methods when you need a new ordering or list state. toSorted(), with(), and toSpliced() don’t mutate cart, but they don’t clone the product objects or their tags arrays either.

copying-methods.js
const cart = [
  { sku: 'tea', qty: 1, tags: ['drink'] },
  { sku: 'mug', qty: 2, tags: ['ceramic'] },
  { sku: 'book', qty: 1, tags: ['paper'] },
];

const ranked = cart.toSorted((a, b) => b.qty - a.qty);
const updated = cart.with(0, { ...cart[0], qty: 3 });
const removed = cart.toSpliced(1, 1);
const snapshot = cart.slice();

snapshot[0].tags.push('featured');

const show = (items) => items.map(
  ({ sku, qty }) => `${sku}:${qty}`,
).join(', ');

console.log(show(ranked));
console.log(show(updated));
console.log(show(removed));
console.log(show(cart));
console.log(cart[0].tags.join(', '));
mug:2, tea:1, book:1
tea:3, mug:2, book:1
tea:1, book:1
tea:1, mug:2, book:1
drink, featured

The first four lines show that the outer arrays can be reordered, shortened, and updated independently. The last line exposes the shallow-copy boundary: snapshot[0] and cart[0] are one object, and their tags value is one array.

with() replaces only the element at the requested index. The object spread in the same expression makes a new first item, so updating qty doesn’t mutate the old object; putting cart[0] straight back would keep every nested reference shared.

Sorting, finding, and short-circuiting

Numeric sorting needs an explicit comparator. Multiple sort keys can be joined with ||, so names are compared only when scores match; find() and findLast() select the first and last matching items.

search-and-sort.js
const scores = [
  { name: 'Lin', score: 9 },
  { name: 'Amir', score: 12 },
  { name: 'Bea', score: 12 },
  { name: 'Zoe', score: 4 },
];

const leaderboard = scores.toSorted(
  (a, b) => b.score - a.score || a.name.localeCompare(b.name, 'en'),
);
const firstPassing = scores.find((entry) => entry.score >= 10);
const lastPassing = scores.findLast((entry) => entry.score >= 10);

let checks = 0;
const hasWinner = scores.some((entry) => {
  checks += 1;
  return entry.score === 12;
});

console.log(leaderboard.map(({ name }) => name).join(' > '));
console.log(firstPassing.name, lastPassing.name);
console.log(hasWinner, checks);
console.log(scores.map(({ name }) => name).join(', '));
Amir > Bea > Lin > Zoe
Amir Bea
true 2
Lin, Amir, Bea, Zoe

some() has its answer after inspecting the second item, so it doesn’t call the callback again. The original scores order remains unchanged because the sort uses toSorted() rather than sort().

Use filter() only when the business needs every match. filter(...)[0] does more iteration and allocates an array to find one item, while find() states the requirement directly; use findIndex() or findLastIndex() when you need the position.

An empty slot isn’t an undefined element

An array can span an index without owning a property at that index; this is a sparse array . Methods don’t handle empty slots uniformly, so length alone doesn’t tell you how often a callback runs.

sparse-arrays.js
const readings = [18, , 21];
const mapVisits = [];
const adjusted = readings.map((value, index) => {
  mapVisits.push(index);
  return value + 1;
});

const findVisits = [];
readings.find((value, index) => {
  findVisits.push(`${index}:${String(value)}`);
  return false;
});

console.log(readings.length, 1 in readings);
console.log(mapVisits.join(','));
console.log(adjusted.length, 1 in adjusted);
console.log(findVisits.join('|'));
console.log(readings.includes(undefined), readings.indexOf(undefined));
3 false
0,2
3 false
0:18|1:undefined|2:21
true -1

map() doesn’t call its callback for the source’s empty slot and preserves a corresponding hole in its result. find() visits every index and passes undefined for the hole; includes(undefined) also treats a hole as undefined, while indexOf(undefined) skips it.

If application data needs an explicit missing value, build a dense array, for example with Array.from({ length: 3 }, () => undefined). Every index then exists, and the method differences can’t silently alter the result.

Pitfalls

Fix: use toSorted() when the input must stay unchanged. Pass (a, b) => a - b for numeric ascending order, and spell out primary and secondary keys for objects; don’t use a comparator that returns only true or false.

Fix: copy only the levels you intend to update, as in items.with(i, { ...items[i], done: true }). If the structure contains deeper mutable objects, define their ownership before choosing selective copies or a dedicated cloning strategy.

Fix: pass 0 for a sum, '' for string concatenation, or [] for list construction. Prefer Map when grouping under dynamic keys; if an API requires a plain object, handle collisions between input keys and the object prototype.

Fix: write the missing-value rule as a predicate. Use value != null to remove only nullish values; if empty strings are also missing, add value !== '' explicitly so the business rule is visible.

Fix: use await Promise.all(items.map(async ...)) when every operation may run in parallel and must finish. Use for...of with await, or an explicit bounded worker design, when execution must be sequential or capacity-limited.

Fix: use Array.from({ length: 3 }, () => ({ pending: true })) when each slot needs an independent object. fill() is a good fit for primitive values such as numbers and strings, or when sharing one reference is intentional.

Deep Callback iteration contracts

Callback iteration contracts

Most iterative methods establish the range they will process before the first callback call. Items appended after iteration begins generally aren’t visited in that pass; deleting or replacing an unvisited slot can affect the value observed later. Code that depends on this is hard to review, so callbacks should avoid changing the array they are traversing.

The callback’s third parameter is the array on which the method was called, not the result under construction. A map() callback can’t use that parameter to access a partly built mapped result; split the work into two named stages or use an explicit accumulator when a later calculation needs an earlier result.

Except for reduce() and reduceRight(), the common callback-based array methods also accept an optional thisArg. A normal function receives that value as this, while an arrow function keeps its lexical this, so thisArg has no effect on an arrow. Capturing the required value directly is usually clearer than relying on dynamic this.

some(), every(), find(), and findIndex() stop when their answer is known. forEach() has no standard early-termination mechanism; when you need break, sequential await, or involved control flow, for...of is often the better construct.

How methods treat sparse arrays

An empty slot means the index property is absent, while an explicit undefined means the property exists with undefined as its value. 1 in values and Object.hasOwn(values, 1) can distinguish them; reading values[1] produces undefined in either case.

map(), forEach(), filter(), some(), and every() don’t invoke callbacks for empty slots. map() preserves holes in its output, while filter() adds only existing elements that pass its predicate and therefore returns a dense result. flat() also removes empty slots at the levels it flattens.

find(), findIndex(), findLast(), and findLastIndex() visit every index in range and pass undefined for an empty slot. includes() likewise treats holes as undefined, but indexOf() and lastIndexOf() skip them. These differences are why sparse inputs need their own tests.

Sparse arrays commonly come from new Array(length), deleting an index, or writing consecutive commas in a literal. When every item needs initialization, use Array.from({ length }, (_, index) => makeValue(index)); calling map() on an empty array creates nothing because no existing slot can trigger the callback.

Reduction and accumulator ownership

reduce() isn’t specifically a sum operation. It folds a sequence into one value, which may be a number, string, array, Map, or domain object; the initial value defines both the empty-input result and the accumulator’s starting type.

Without an initial value, the method searches for the first existing item and uses it as the accumulator. It throws TypeError if no item exists, and with one existing item it never calls the callback. Those rules are particularly surprising for sparse arrays, so application code should usually supply the initial value.

An accumulator can be updated in place or replaced on every iteration. [...accumulator, item] copies the existing content on each callback, while push() on an array owned by this reduction reuses one accumulator. The right choice depends on whether the accumulator belongs only to this call; don’t apply an “immutable” label mechanically to temporary internal state.

When grouping under external strings, a Map expresses arbitrary keys directly and avoids inherited properties on plain objects. Convert at the boundary if a downstream API requires an object; don’t let generated code write unchecked input strings into {}.

Copying methods and element identity

slice(), concat(), spread syntax, Array.from(), toSorted(), toReversed(), and toSpliced() all create a new outer array. map() and filter() do too, though their own rules decide which elements remain and whether a transformation callback runs.

An outer copy isolates sorting, insertion, removal, and index replacement. It doesn’t isolate assignments to element objects, so state updates often use two layers: map() or with() creates an array, and a new object is created only for an element that changes.

Deep copying isn’t a guarantee that any array method provides. A cloning strategy has to account for value types, prototypes, cycles, and whether objects such as Date, Map, or binary data must survive; JSON round-tripping isn’t a general answer for all JavaScript values.

A method chain isn’t automatically safer code either. Each stage should have a clear input and output contract; once a chain contains side effects, an async boundary, or an accumulator that’s hard to name, splitting it usually makes verification easier.

Indexes, ranges, and editing

Array range methods usually include the start index and exclude the end index. slice(start, end) reads that range and returns a new array, while splice(start, deleteCount, ...items) removes or inserts items in the original; their names sound alike, but their second parameters mean different things.

OperationMethodReturn valueOriginal changes
Read one indexat(index)Item or undefinedNo
Copy a rangeslice(start, end)Shallow copy of the rangeNo
Remove or insert in placesplice(start, deleteCount, ...items)Array of removed itemsYes
Remove or insert in a copytoSpliced(start, deleteCount, ...items)Updated new arrayNo
Replace one index in a copywith(index, value)Updated new arrayNo
Add or remove at the endpush(...items), pop()New length or removed itemYes
Add or remove at the startunshift(...items), shift()New length or removed itemYes

at() and with() accept negative indexes, with -1 selecting the last item. Plain array[-1] isn’t a from-the-end index; it’s an object property named "-1", which regular array iteration ignores and which doesn’t change length.

slice() resolves negative boundaries relative to the end of the array. slice(-2) copies the last two items, while slice(1, -1) copies from index 1 up to the last item. It doesn’t delete anything from the source.

The second argument to splice() is a deletion count, not an end index. items.splice(2, 1) deletes one item at index 2, while items.splice(2, 0, value) inserts there without deleting; generated code that treats this like a slice() boundary often removes too much.

push() and unshift() return the new length, while pop() and shift() return the removed item; the last two return undefined on an empty array. Don’t infer that a mutating method also returns its array: check the return contract before chaining.

Creating and recognizing arrays

The static methods on Array create or identify arrays rather than acting on one existing array’s instance state. They often bring iterators, array-like objects, and asynchronous sources into a common array pipeline.

NeedStatic methodMain result
Recognize a genuine arrayArray.isArray(value)Boolean
Create from an iterable or array-like objectArray.from(source, mapFn?)New array
Create directly from argumentsArray.of(...items)New array
Create from an async or sync source and await valuesArray.fromAsync(source, mapFn?)Promise for a new array

Array.isArray() is a better array check than value instanceof Array. An array from another browser realm has a different Array constructor and may fail the current realm’s instanceof, while Array.isArray() still recognizes its array internal slot.

Array.from() reads strings, Set, Map, iterators, and array-like objects with a length. Its optional mapping function runs during construction, avoiding an intermediate array before map(); this is still an element transformation, not a deep copy.

Array.of(3) creates [3], while new Array(3) creates a sparse array of length 3. The difference is most dangerous with one numeric argument; an array literal is usually more direct, while Array.of() suits factories whose callers determine the argument count.

Array.fromAsync() returns a Promise and accepts async iterables, sync iterables, or array-like sources. It awaits yielded values and mapping results, but it isn’t the same as starting unbounded work over an existing array with Promise.all(); choose only after defining the source’s iteration and capacity semantics.

Generic methods and array-like objects

Many Array.prototype methods are generic: they read length and integer keys without requiring the receiver to be a genuine array. DOM collections and arguments can sometimes borrow such a method, though converting once with Array.from() is usually easier to understand and pass around.

Generic doesn’t mean every receiver can be safely mutated. Methods such as push() and splice() need to write indexes and length; strings are immutable, and objects with a read-only length or constrained properties can also throw. Check the object’s property constraints before borrowing a method.

Typed arrays have a similar method set but a fixed length, specific numeric element types, and different construction rules. A shared method name doesn’t make every Array and TypedArray edge case identical; follow the typed-array contract when working with binary data.

When a loop is clearer

Array methods work well when one method name accurately describes the result. A loop can state the control flow more directly when you need an early break, sequential asynchronous work, several related accumulators, or an explicit distinction between holes and undefined.

for...of reads values in sequence and is a natural place for sequential await, but on a sparse array it yields undefined for a hole. If you need to know whether an index exists, iterate indexes and check with Object.hasOwn().

Don’t rewrite a readable loop as a nested reduce() merely to save lines. Choose a method because its return contract and side effects are clear, not because the callback count or chain is shorter.

Naming intermediate results

A short chain can make data flow obvious, while a long one hides each stage’s assumptions. If a stage has business meaning or deserves its own logging, test, or reuse, assign it to a meaningful constant.

Naming a result doesn’t change method semantics or automatically remove copying costs. Its value is a review boundary where empty inputs, sort stability, and shared elements can be checked one stage at a time.

Further reading

checkpoint

4 questions · 1 predict-the-output · 1 spot-the-bug

Copy as Markdown Interview bank Edit on GitHub Report an error Was this clear?