Mutating vs Non-Mutating Interview Questions & Answers
31 questions Updated 2026-06-18
JavaScript array mutation interview questions — which methods mutate versus return a new array, immutability patterns, copying arrays, the new toSorted and with methods, and structuredClone for nested data.
Read the in-depth guideMutating vs Non-Mutating Array Methods in JavaScript — Immutability Done RightA mutating method changes the original array in place rather than returning a fresh copy. The variable still points to the same array, but its contents have changed.
const arr = [1, 2, 3]
arr.push(4) // mutates: arr is now [1, 2, 3, 4]
This matters because other code holding a reference to the same array sees the change too — a frequent source of surprising bugs in shared state.
The classic mutators all change the array in place:
push, pop, shift, unshift -> add/remove ends
splice -> insert/remove/replace anywhere
sort, reverse -> reorder
fill, copyWithin -> overwrite ranges
A memory aid: if a method changes length or order, it almost certainly
mutates. The notable surprise is that sort and reverse mutate — many
assume they return a new array like map.
These leave the original untouched and return a new array:
map, filter, slice, concat, flat, flatMap
toSorted, toReversed, toSpliced, with (ES2023)
Plus the spread copy [...arr]. As a rule, the iteration/transform methods
are non-mutating, while the in-place edit methods mutate. Knowing which is
which is essential for writing predictable code.
push mutates the array and returns the new length; concat returns a new array and leaves the original alone.
const a = [1, 2]
a.push(3) // a is [1,2,3], returns 3
const b = a.concat(4) // b is [1,2,3,4], a unchanged
Pitfall: people sometimes write arr = arr.push(x), which sets arr to a
number (the length). Use concat or spread when you want a new array
reference.
slice is non-mutating: it returns a shallow copy of a range. splice mutates: it removes/inserts elements in place and returns the removed ones.
const arr = [1, 2, 3, 4]
arr.slice(1, 3) // [2, 3], arr unchanged
arr.splice(1, 2) // removes [2, 3], arr is now [1, 4]
The names look alike but behave oppositely on mutation. Confusing them is a classic bug — remember slice = copy, splice = surgery.
Yes. sort sorts the array in place and also returns a reference to that same array — so both the original and the return value are sorted.
const arr = [3, 1, 2]
const sorted = arr.sort()
sorted === arr // true — same array
arr // [1, 2, 3] — original is changed
To sort without mutating, copy first ([...arr].sort()) or use toSorted()
in modern environments. The same applies to reverse.
toSorted (ES2023) returns a new sorted array and leaves the original
unchanged — the immutable counterpart of sort.
const arr = [3, 1, 2]
const sorted = arr.toSorted() // [1, 2, 3]
arr // [3, 1, 2] — untouched
It takes the same optional comparator as sort. Use it to avoid the classic
[...arr].sort() copy dance, especially in React state where mutating is a
bug.
They are the immutable versions of reverse, splice, and index
assignment, all returning a new array (ES2023).
const arr = [1, 2, 3]
arr.toReversed() // [3, 2, 1], arr unchanged
arr.toSpliced(1, 1, 9) // [1, 9, 3], arr unchanged
arr.with(0, 100) // [100, 2, 3], arr unchanged
with(i, value) is the immutable replacement for arr[i] = value. Together
these let you write update logic without copying first.
When two variables reference the same array, mutating through one changes what the other sees. Functions that mutate their arguments cause action at a distance.
const original = [1, 2, 3]
const sorted = original
sorted.sort((a, b) => b - a)
original // [3, 2, 1] — caller's array got reordered
Frameworks like React rely on referential equality to detect changes; mutating in place keeps the same reference, so the UI may not re-render. Returning new arrays avoids both classes of bug.
For a shallow copy, any of these work:
const a = [1, 2, 3]
const b = [...a] // spread (most common)
const c = a.slice() // no args = full copy
const d = Array.from(a) //
const e = a.concat() //
All four copy the top level only. If the array holds objects, the copies share those objects — see deep copying for nested data.
A shallow copy duplicates the array but shares the same nested object references. A deep copy duplicates everything recursively.
const arr = [{ n: 1 }]
const shallow = [...arr]
shallow[0].n = 99
arr[0].n // 99 — nested object is shared
const deep = structuredClone(arr)
deep[0].n = 1
arr[0].n // 99 — independent
Spread/slice are shallow; reach for structuredClone when nested data must be
independent.
structuredClone does a true deep copy of arrays containing nested objects, arrays, Maps, Sets, Dates, and more — without the limitations of JSON tricks.
const data = [{ id: 1, tags: ['a'] }]
const copy = structuredClone(data) // fully independent
Caveat: it cannot clone functions, DOM nodes, or class instances with methods (it throws or drops the prototype). It's built into modern Node and all current browsers.
It deep-copies plain data but silently corrupts anything non-JSON:
const arr = [{ d: new Date(), fn: () => {}, u: undefined, n: NaN }]
JSON.parse(JSON.stringify(arr))
// Date -> string, fn -> dropped, undefined -> dropped, NaN -> null
It also throws on circular references. Prefer structuredClone for deep
copies; reserve the JSON trick for arrays of plain, JSON-safe values where the
conversions don't matter.
Build a new array with spread instead of push/unshift:
const arr = [1, 2, 3]
const appended = [...arr, 4] // add to end
const prepended = [0, ...arr] // add to start
arr // [1, 2, 3] — unchanged
This pattern is standard in Redux reducers and React setState, where mutating
the existing array would break change detection and time-travel debugging.
Use filter (by value/predicate) or slice + spread (by index) to produce a new array.
const arr = [1, 2, 3, 4]
arr.filter(n => n !== 3) // [1, 2, 4] by value
const i = 2
[...arr.slice(0, i), ...arr.slice(i + 1)] // [1, 2, 4] by index
arr.toSpliced(i, 1) // [1, 2, 4] ES2023
Avoid splice, which mutates. toSpliced is the cleanest modern option for
index-based removal.
Use map with the index, or with() in modern environments — never
arr[i] = x, which mutates.
const arr = [1, 2, 3]
arr.map((v, idx) => (idx === 1 ? 99 : v)) // [1, 99, 3]
arr.with(1, 99) // [1, 99, 3] ES2023
arr // [1, 2, 3] — unchanged
For arrays of objects, also spread the object: arr.map(o => o.id === id ? { ...o, done: true } : o) to avoid mutating the nested object.
It makes it shallowly immutable: you can't add, remove, or reassign top-level elements, but nested objects stay mutable.
const arr = Object.freeze([{ n: 1 }])
arr.push(2) // throws in strict mode (or silently fails)
arr[0].n = 99 // still works — nested object not frozen
For deep immutability you must freeze recursively (a "deep freeze"). Also note
mutating methods like push throw on a frozen array in strict mode.
fill overwrites a range of the array with a static value in place — it mutates and returns the same array.
const arr = [1, 2, 3, 4]
arr.fill(0, 1, 3) // [1, 0, 0, 4], arr mutated
new Array(3).fill(0) // [0, 0, 0] — common init pattern
Gotcha: filling with a shared object puts the same reference in every
slot: new Array(3).fill([]) gives three references to one array. Use
Array.from({length:3}, () => []) for distinct objects.
copyWithin(target, start, end) copies a slice of the array to another position within the same array, in place, without changing its length.
const arr = [1, 2, 3, 4, 5]
arr.copyWithin(0, 3) // [4, 5, 3, 4, 5] — copy from index 3 to 0
It mutates and is rarely used in app code — it exists mainly for high performance buffer manipulation. Worth recognizing in interviews but seldom reached for in practice.
Yes — reverse reverses the array in place and returns a reference to the same array.
const arr = [1, 2, 3]
arr.reverse()
arr // [3, 2, 1] — original changed
To reverse without mutating, use [...arr].reverse() or toReversed(). This
mutation surprises people who expect array methods to be functional like map.
Arrays are reference types. Assignment copies the reference, not the data, so both names point at the same array.
const a = [1, 2, 3]
const b = a
b.push(4)
a // [1, 2, 3, 4] — same array
To get an independent array you must explicitly copy: const b = [...a]. This
reference behavior underlies most accidental-mutation bugs.
The function receives the same reference, so any mutating method affects the caller's array.
function addItem(arr) { arr.push('x') } // mutates caller
const list = [1]
addItem(list)
list // [1, 'x']
Write pure functions that return new arrays instead: function addItem(arr) { return [...arr, 'x'] }. This keeps call sites free of surprises and makes the
code easier to test.
Shallow. Spread copies the top-level elements; nested objects/arrays are still shared by reference.
const arr = [[1], [2]]
const copy = [...arr]
copy[0].push(99)
arr[0] // [1, 99] — inner array shared
For nested data you need a deep copy (structuredClone) or to spread each
level. Treat spread as a fast top-level clone, nothing more.
React decides whether to re-render by comparing the previous and next reference. Mutating in place keeps the same reference, so React thinks nothing changed.
// mutation — same reference, no re-render
items.push(newItem); setItems(items)
// new reference — triggers re-render
setItems([...items, newItem])
Always create a new array (map, filter, spread, toSorted, with) when
updating state so React sees the change.
Assigning to length mutates in place: shrinking truncates elements,
growing pads with empty slots, and length = 0 empties the array.
const arr = [1, 2, 3, 4]
arr.length = 2 // [1, 2] — truncated
arr.length = 0 // [] — cleared
arr.length = 0 is a fast in-place clear that affects every reference to the
array. To clear without mutating shared references, reassign: arr = [].
Both mutate and return the removed element. pop removes from the end,
shift from the start.
const arr = [1, 2, 3]
arr.pop() // returns 3, arr is [1, 2]
arr.shift() // returns 1, arr is [2]
Note shift/unshift are O(n) because every remaining element must be
reindexed, whereas push/pop are O(1). Prefer end operations in hot loops.
Mostly yes — both return a new array without mutating. Spread reads more
naturally; concat has one nicety: it accepts non-array values directly.
[...a, ...b] // merge two arrays
a.concat(b) // same result
a.concat(b, 5, 6) // concat flattens array args and appends values
[...a, b] // pushes the array b as a single element
For huge arrays concat can be marginally faster, but readability usually
favors spread.
The clean, non-mutating way uses a Set:
const unique = [...new Set(arr)] // new array, order preserved
An in-place dedupe is fiddly and error-prone (you'd splice while iterating,
which shifts indices). Prefer the Set approach. Note it uses SameValueZero,
so NaN is deduped correctly but distinct objects with equal contents are not.
They let you write mutating-looking code that actually produces a new, immutable array under the hood, avoiding verbose spread chains for deeply nested updates.
const next = produce(state, draft => {
draft.items.push(newItem) // looks like mutation
}) // state is untouched, next is a new tree
Immer tracks changes to a proxy "draft" and builds the new structure for you. It's popular in Redux Toolkit precisely because deep immutable updates with spread get unwieldy.
Use Object.freeze in development to make accidental mutation throw, lean on const (which prevents reassignment, not mutation), and adopt lint rules or libraries that enforce immutability.
const config = Object.freeze([1, 2, 3])
config.push(4) // throws in strict mode — caught early
In TypeScript, readonly arrays and as const give compile-time
protection with zero runtime cost, catching mutation before it ships.
Prefer immutable updates whenever the array is shared — across components, stored in framework state, or passed between modules — because new references make change tracking and reasoning reliable.
// shared state -> immutable
setItems(items.filter(i => i.id !== id)) //
Mutation is fine for short-lived local arrays you fully own inside a function, where in-place edits are faster and no one else can observe them.
Practice tests are coming soon
Get notified when interactive mock interviews and quizzes launch.