Topic 5.2
Adding, Removing and Changing
In one line
push/pop work at the end of an array, unshift/shift at the start, and splice anywhere in the middle; all of them change (mutate) the original array, while methods like slice, concat, with and toSpliced leave it alone and return a new one.
Think of it like this
A stack of plates and a queue at a ticket counter. You put a clean plate on top of the stack and take the top one off: that's push and pop, working at the end of an array. In a queue, people join at the back (push) and the person at the front leaves first (shift). Someone pushing in at the front is unshift. And a shop assistant who pulls two people out of the middle of the queue and puts a new one in their place is splice.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Mutate
- Change something in place, so everyone holding it sees the change.
- Non-mutating
- Leaves the original untouched and gives you back a new array instead.
- Stack
- A pile where the last thing added is the first one taken out (
push+pop). - Queue
- A line where the first thing added is the first one taken out (
push+shift). - splice
- The method that removes and/or inserts items at any position, changing the array.
- slice
- The method that copies part of an array into a new one, without changing the original.
- Return value
- What a method gives back when it finishes, for example the new length from
push.
Step by step
01Work at the end
push adds, pop removes. Look at what each one returns: push gives the new length, not the array.
const stack = ["a"];
stack.push("b", "c"); // returns 3
stack.pop(); // returns "c"
// stack is now [ 'a', 'b' ]02Work at the start
unshift adds at the front, shift removes from the front. Every other item moves to a new index.
03Work in the middle with splice
Read splice(1, 2, "X") as: "go to index 1, remove 2 items, put X there". The removed items come back as an array.
const a = [10, 20, 30, 40];
const gone = a.splice(1, 2, "X");
// gone is [ 20, 30 ]
// a is [ 10, 'X', 40 ]04Copy instead of change
When you must keep the original, use the non-mutating twin. toSpliced takes the same arguments as splice but returns a new array; with(i, v) is a copy with one slot replaced.
const b = [10, 20, 30];
const c = b.with(1, 99); // [ 10, 99, 30 ]
const d = b.toSpliced(0, 1); // [ 20, 30 ]
// b is still [ 10, 20, 30 ]05Pick the right tool
End: push/pop. Start: unshift/shift. Middle: splice (or toSpliced for a copy). Replace one item: arr[i] = v (or with for a copy). Part of the array: slice. Join arrays: concat or spread (Topic 5.8).
Try it yourself
- 1
Undo the last join
In the first example, add a third button that removes the last person in the queue. Which method do you need? Predict what it returns when the queue is empty, then try it.
- 2
Insert in the middle
In the second example, before the last
splice, insert"M"between"X"and"Y"with onesplicecall. Predict the full array first. - 3
Swap a method for its copy
In the third example, replace
original.with(0, 99)withoriginal[0] = 99. Predict all the lines now. Which ones change, and why?
Code & diagrams
Click the buttons: joining uses `push` (add at the back), serving uses `shift` (remove from the front). The console part shows what each method returns.
Preview
Console
Nothing logged yet.Expected console output
3
Meera
3
Dev
[ 'Asha', 'Ravi' ]
undefinedEach call changes `letters`. Only the first one keeps what `splice` returned.
Console
(nothing was printed)Expected console output
[ 'b', 'c' ] [ 'a', 'd', 'e' ]
[ 'a', 'X', 'Y', 'd', 'e' ]
[ 'a', 'X', 'Y', 'd', 'Z' ]
[ 'a', 'X' ]`with` and `toSpliced` are ES2023. The original is untouched by all four copying methods. `delete` leaves a hole; `length = 0` empties the array for everyone who holds it.
Console
(nothing was printed)Expected console output
[ 3, 1, 2 ]
[ 3, 1 ] [ 3, 1, 2, 4, 5 ] [ 99, 1, 2 ] [ 1, 2 ]
[ 'a', <empty>, 'c' ] 3
[]Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Expecting push to return the array
Write const list = [1, 2].push(3); console.log(list.length);
Break #2
Mixing up slice and splice
To read the first two items for a preview, write const preview = items.splice(0, 2);.
Myth vs fact
Myth
delete arr[i] removes an item from an array.
Fact
It removes the value and leaves a hole; length doesn't change and later items don't move. Use splice(i, 1) or toSpliced(i, 1).
Myth
slice and splice are two spellings of the same thing.
Fact
slice(start, end) copies part of the array and never changes it. splice(start, count, ...items) removes and inserts in place and returns what it removed.
Myth
shift is as fast as pop.
Fact
pop touches only the last slot. shift must move every remaining element one place to the left, so it gets slower as the array grows.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
pushandpopare amortised O(1): the engine keeps spare capacity and only reallocates (usually growing by about 1.5×) when it runs out.shift/unshift/splicein the middle are O(n). V8 has tricks forshifton small arrays, but don't rely on them for big queues; use a ring buffer or two-index queue. - ▸
The ES2023 change-by-copy methods (
toSorted,toReversed,toSpliced,with) also exist on typed arrays (excepttoSpliced).withthrows aRangeErrorfor an index outside the array instead of growing it, unlikearr[i] = v. - ▸
slice()with no arguments and[...arr]both make a shallow copy.slicekeeps holes as holes; spread turns holes intoundefinedvalues. - ▸
fill(value)puts the same value in every slot.new Array(3).fill([])gives three references to one inner array, a classic bug; useArray.from({ length: 3 }, () => [])(Topic 5.9).
Remember this
- 1
push(a, b, ...)adds one or more items to the end and returns the new length.pop()removes the last item and returns that item (orundefinedif the array was empty). They are fast, because nothing else has to move. Together they make an array work as a stack (last in, first out). - 2
unshift(a, b, ...)adds items to the start and returns the new length.shift()removes the first item and returns it. These are slower on long arrays, because every other element has to move along one place to get a new index.push+shiftmakes a queue (first in, first out); for very large queues a different data structure is better (see Hectal's/dsacourse). - 3
splice(start, deleteCount, ...newItems)is the all-in-one tool for the middle. It removesdeleteCountitems starting at indexstart, inserts anynewItemsin their place, and returns an array of the removed items.splice(1, 0, "x")inserts without removing;splice(2)with no count removes everything from index 2 to the end; a negativestartcounts from the end. Don't confuse it withslice, which only copies. - 4
A method that changes the array it's called on mutates it ("mutate" means change in place).
push,pop,shift,unshift,splice,sort,reverse,fillandcopyWithinall mutate. Non-mutating methods leave the original alone and return a new array:slice(start, end)(a copy of part of the array,endnot included),concat(...)(joined copy), and the ES2023 "change by copy" methodswith(index, value),toSpliced(...),toSorted()andtoReversed(). - 5
Why it matters: when two parts of your program hold the same array (Topic 5.10 explains references), mutating it changes what both of them see. Frameworks like React decide whether to redraw the screen by checking whether you gave them a new array, so mutating in place can make the screen fail to update. A good habit: mutate arrays you just created yourself; return new arrays from functions that receive arrays from somewhere else.
- 6
Two traps:
delete arr[1]removes the value but leaves a hole and keepslengththe same; usesplice(1, 1)instead. And to empty an array that others share,arr.length = 0clears it in place, whilearr = []only points your own variable at a new empty array (and fails ifarrisconst).
Explain it without notes
What do push, pop, shift and unshift return?
Explain the arguments and return value of splice.
What is the difference between a mutating and a non-mutating array method, and why does it matter?
Why is shift slower than pop on a large array?
Practice
Start with const tasks = ["wash", "cook"];. Add "shop" at the start and "sleep" at the end, then remove "cook" with splice. Print the array.
Write a function removeAt(arr, i) that returns a new array without the item at index i and doesn't change arr.
Trade-offs
- ↔
Mutating in place uses no extra memory and is fastest; returning copies is safer when an array is shared, and is what React and other state libraries expect, at the cost of allocating a new array each time.
- ↔
unshift/shiftare convenient but O(n); for big queues a head index or a dedicated queue structure is faster.
Done when you can
I can add and remove items at the start and end and know what each method returns.
I can remove, insert and replace items in the middle with
splice.I can tell
slicefromsplice.I can name which array methods mutate and which return a new array.
I can use
withandtoSplicedto change a copy instead of the original.