# Array methods

Source: https://codewiki.com/javascript/array-methods/

> - **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.

| Goal | Common methods | Result | Mutates original |
| --- | --- | --- | --- |
| Perform one side effect per item | `forEach()` | `undefined` | No |
| Transform or filter items | `map()`, `filter()`, `flatMap()` | New array | No |
| Find an item or index | `find()`, `findLast()`, `findIndex()` | Item, index, or not-found marker | No |
| Test a condition | `some()`, `every()`, `includes()` | Boolean | No |
| Aggregate | `reduce()`, `reduceRight()` | Any accumulated result | Callback-dependent |
| Extract or combine | `slice()`, `concat()`, `flat()` | New array | No |
| Add, remove, or reorder in place | `push()`, `pop()`, `splice()`, `sort()`, `reverse()` | Method-dependent | Yes |
| Add, remove, or reorder a copy | `toSpliced()`, `toSorted()`, `toReversed()`, `with()` | New array | No |

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()`.

<!-- quick -->

```js
// file: 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-')));
```

```text
[ 'A-101: EUR 48', 'A-103: EUR 80' ]
128
true
true
```

<!-- /quick -->

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.

```js
// file: 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(', '));
```

```text
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.

```js
// file: 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(', '));
```

```text
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.

```js
// file: 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));
```

```text
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

> **Pitfall:** `sort()` mutates its array, and without a comparator it compares elements as strings. The default order of the numeric array `[2, 10, 1]` isn't numeric ascending order.

**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`.

> **Pitfall:** `slice()`, spread syntax, `concat()`, and copying array methods make shallow copies. Mutating an element object afterward also changes that same object as observed through the old array.

**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.

> **Pitfall:** Omitting the `reduce()` initial value throws a `TypeError` when the array might be empty. For a non-empty input, the first existing item becomes the accumulator and callbacks begin with the next existing item, which can also make accumulator types unstable.

**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.

> **Pitfall:** `filter(Boolean)` removes more than `null` and `undefined`: it also drops valid `0`, `false`, empty strings, and `NaN`. Generated cleanup code can silently lose data this way.

**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.

> **Pitfall:** `forEach(async (item) => ...)` doesn't await the Promises returned by its callbacks. Outer code continues first, and `forEach()` doesn't collect callback rejections.

**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.

> **Pitfall:** `new Array(3).fill({ pending: true })` stores one object reference in all three slots. Updating one item makes all three appear to change.

**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

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.

| Operation | Method | Return value | Original changes |
| --- | --- | --- | --- |
| Read one index | `at(index)` | Item or `undefined` | No |
| Copy a range | `slice(start, end)` | Shallow copy of the range | No |
| Remove or insert in place | `splice(start, deleteCount, ...items)` | Array of removed items | Yes |
| Remove or insert in a copy | `toSpliced(start, deleteCount, ...items)` | Updated new array | No |
| Replace one index in a copy | `with(index, value)` | Updated new array | No |
| Add or remove at the end | `push(...items)`, `pop()` | New length or removed item | Yes |
| Add or remove at the start | `unshift(...items)`, `shift()` | New length or removed item | Yes |

`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.

| Need | Static method | Main result |
| --- | --- | --- |
| Recognize a genuine array | `Array.isArray(value)` | Boolean |
| Create from an iterable or array-like object | `Array.from(source, mapFn?)` | New array |
| Create directly from arguments | `Array.of(...items)` | New array |
| Create from an async or sync source and await values | `Array.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.

<!-- /deep -->

[Checkpoint: javascript/array-methods](https://codewiki.com/javascript/array-methods/#checkpoint)

## Further reading

- [MDN: `Array` reference](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array)
- [MDN: indexed collections guide](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Indexed_collections)
- [MDN: `Array.prototype.toSorted()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/toSorted)
- [MDN: `Array.prototype.reduce()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce)
- [MDN: `Array.prototype.sort()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort)
