skip to content

Copying vs Mutating Methods

Half the array methods change the array in place and half return a new one, and mixing them up is the classic source of "my React state didn't update" bugs. The ES2023 copying methods — toSorted, toReversed, toSpliced, with — finally give you immutable counterparts, and interviewers like asking which of a pair mutates.

part ofJavaScriptoverview, primer and where to startread it →
on this pageshow

questions

5

In JavaScript, what is the difference between Array.prototype.slice() and Array.prototype.splice(), and what does each one return?

level: juniorimportance: must knowfreq 80%

answer

  1. one letter apart, opposite behaviour
  2. one copies a range, one edits in place
  3. check what each hands back
  4. splice returns what was removed
  5. slice(0,3) reads, splice(0,3) removes

basics

~20 s

slice() copies: it leaves the original array untouched and returns a new array holding the selected range. splice() edits in place: it removes and/or inserts elements in the original array and returns an array of the elements it removed.

solid answer

~40 s

`slice(start, end)` is a copying method. It returns a new array with the elements from `start` up to but not including `end`, supports negative indexes counted from the end, and never touches the receiver — `arr.slice()` with no arguments is the classic full shallow copy idiom. `splice(start, deleteCount, ...items)` is a mutating method: it deletes `deleteCount` elements at `start`, inserts any extra arguments there, changes `length` and reindexes the array — and its return value is the array of *removed* elements, not the array you spliced. So `list.slice(0, 3)` hands you the first three items and leaves `list` alone, while `list.splice(0, 3)` hands you the same three items but leaves `list` three elements shorter. Confusing the two is one of the most common accidental-mutation bugs in JavaScript.

code

javascript · 10 lines
javascript
const list = ['a', 'b', 'c', 'd'];

const copied = list.slice(1, 3);
console.log(copied);            // ['b', 'c']
console.log(list.length);       // 4 — original intact

const removed = list.splice(1, 2, 'X');
console.log(removed);           // ['b', 'c'] — what came out
console.log(list);              // ['a', 'X', 'd'] — mutated
console.log(list.length);       // 3

go deeper

for a junior

Be ready to state plainly that slice copies and splice mutates, and to say what each returns: a new array of the selected range versus the elements that were removed.

for a middle

Explain the argument shapes — slice takes start and end while splice takes start and deleteCount plus insertions — and demonstrate the insert idiom splice(i, 0, x) and negative-index handling.

for a senior

Show the judgment of not mutating arrays you do not own: copy at function boundaries, treat splice's return value as "what came out", and name toSpliced as the copying counterpart.

for a principal

Frame it as an API-contract decision: whether your utilities take ownership of arguments or promise not to mutate them, and how you make that contract obvious in naming, documentation and review.

## Two similar names, opposite behaviour JavaScript arrays are mutable objects, and the array methods fall into two families: **mutating** methods that change the array they are called on, and **copying** methods that leave it alone and hand back a new array. `slice` and `splice` sit on opposite sides of that line while differing by one letter, which is exactly why interviewers pair them. ## slice(start, end) — copying `slice` returns a **new** array containing the elements from index `start` up to but *not including* `end`. Both arguments are optional: `start` defaults to `0`, `end` defaults to `length`. Negative values count back from the end of the array. ```js const a = ['a', 'b', 'c', 'd']; console.log(a.slice(1, 3)); // ['b', 'c'] console.log(a.slice(-2)); // ['c', 'd'] console.log(a); // ['a','b','c','d'] — untouched const full = a.slice(); // full shallow copy ``` The copy is **shallow**: the new array holds the same element values, so for object elements both arrays point at the same objects. ## splice(start, deleteCount, ...items) — mutating `splice` edits the receiver in place. It removes `deleteCount` elements beginning at `start` and inserts any further arguments at that position, shifting the remaining elements and updating `length`. Its **return value is an array of the removed elements**. ```js const a = ['a', 'b', 'c', 'd']; const removed = a.splice(1, 2, 'X'); console.log(removed); // ['b', 'c'] <- what came out console.log(a); // ['a', 'X', 'd'] <- the array itself changed a.splice(1, 0, 'y'); // deleteCount 0 = pure insert console.log(a); // ['a', 'y', 'X', 'd'] a.splice(2); // omitting deleteCount removes to the end console.log(a); // ['a', 'y'] ``` A negative `start` counts from the end, and a `start` past `length` clamps to the end, so `arr.splice(arr.length, 0, x)` is an awkward `push`. ## Where the confusion bites The classic bug is reaching for `splice` when you only wanted to *read* a range: ```js function topThree(list) { return list.splice(0, 3); // BUG: shrinks the caller's list } ``` The function returns the right three items, so a quick test passes — but every call permanently removes three elements from the array that was passed in. `list.slice(0, 3)` is the correct call. The mirror-image mistake is expecting `splice` to hand back the *edited* array, which leads to `const updated = arr.splice(1, 1)` and then wondering why `updated` has one element in it. ## Return-value cheat sheet for the neighbours It helps to memorise the whole in-place family, because their return values are all different: `push`/`unshift` return the new `length`, `pop`/`shift` return the single removed element, `splice` returns an array of removed elements, and `reverse`/`sort`/`fill`/`copyWithin` return **the same array object** they just mutated. Meanwhile `slice`, `concat`, `map`, `filter`, `flat` and `flatMap` all return fresh arrays. ## The ES2023 copying counterpart ES2023 added `toSpliced(start, skipCount, ...items)`, the non-mutating twin of `splice`. Note the return value flips: `toSpliced` returns the **resulting** array and throws the removed elements away, whereas `splice` returns the removed elements and keeps the result in place. ```js const a = ['a', 'b', 'c', 'd']; const b = a.toSpliced(1, 2, 'X'); console.log(b); // ['a', 'X', 'd'] console.log(a); // ['a','b','c','d'] — untouched ``` ## Practical guidance If the array is shared — a function parameter, a value stored in a container someone else reads, anything you did not create in the current function — prefer the copying method and hand back a new array. Reach for `splice` when you deliberately own the array and want an in-place edit, and read its return value as "what came out", never as "the new array".

  • What does splice return if deleteCount is 0?
    An empty array. With `deleteCount` of `0`, `splice` removes nothing and only inserts the extra arguments, so the "removed elements" array it returns has no members — but the receiver is still mutated by the insertion. It is the standard idiom for inserting at an index: `arr.splice(2, 0, 'x')`.
  • How would you delete one element from an array without mutating it?
    Copy around the index: `arr.slice(0, i).concat(arr.slice(i + 1))`, or in an ES2023 runtime simply `arr.toSpliced(i, 1)`, which returns the resulting array and leaves the original alone. Avoid `delete arr[i]` — that leaves a hole and does not change `length`.
  • Is the array returned by slice() safe to mutate freely?
    The array object itself is new, so pushing, sorting or reversing it will not affect the original. But the copy is shallow: the elements are the same references, so mutating a nested object such as `copy[0].name = 'x'` is visible through the original array too.

saying these in an interview costs you the question

  • Thinking splice returns the modified array
  • Saying slice mutates because it changes indexes
  • Using splice(0, 3) to peek at the first three items
  • Believing both methods take (start, end) arguments
  • Assuming the array from slice() deep-copies nested objects

context

open as a page

You copy an array of objects with slice(), then edit a property on one element of the copy — and the original array shows the change too. Why, and how do you get a copy that does not share nested data?

level: seniorimportance: must knowfreq 65%

basics

~20 s

Array copy methods such as slice, concat and map are shallow: they copy element references, so nested objects remain shared between the two arrays and editing one shows through both. A deep copy needs structuredClone or explicit per-level copying.

open as a page

What are the ES2023 array methods toSorted, toReversed, toSpliced and with, and how do they differ from their older counterparts?

level: middleimportance: should knowfreq 40%

basics

~20 s

ES2023 added four copying array methods — toSorted, toReversed, toSpliced and with — that return a new array instead of editing the receiver, giving non-mutating counterparts to sort, reverse, splice and assignment to an index.

open as a page

What do JavaScript's in-place array methods such as push, pop, splice, reverse and fill actually return, and what bug follows from assuming they return a new array?

level: middleimportance: should knowfreq 55%

basics

~20 s

Mutating array methods rarely return a new array: push and unshift return the new length, pop and shift return the removed element, splice returns the removed elements, and reverse, sort, fill and copyWithin return the same array object they just changed.

open as a page

A helper function receives an array parameter and calls reverse() on it before returning a slice. What goes wrong for the caller, and how should the function be written instead?

level: seniorimportance: should knowfreq 45%

basics

~20 s

Calling an in-place method such as reverse, sort, splice or fill on an array parameter edits the caller's own array, causing changes the caller never asked for. Copy first — with slice() or an ES2023 copying method — before reordering shared input.

open as a page