Structured data: arrays and objects · Lesson 23 of 77 · practice
Changing a list: adding, removing, copying
About 60 minutes.
By the end of this lesson you can
- Add and remove elements at either end with push, pop, unshift and shift, and say what each one gives back.
- Explain the difference between a method that changes an array in place and one that returns a new array.
- Use slice to take a copy, or a section, without touching the original.
- Tell slice and splice apart, and pick the right one when the array belongs to someone else.
- Build a new array from an old one with concat instead of mutating it.
Every array method is one of two kinds
A mental model
Arrays come with a lot of built-in methods, and learning them one at a time as a list of names is miserable. There is a much better organising idea: every one of them either changes the array you called it on, or leaves it completely alone and hands you a new array.
MDN groups them under exactly those two headings — copying methods and mutating methods. Once you know which group a method is in, its behaviour stops being something to memorise. `push` is mutating, so it changes your array. `slice` is copying, so it cannot.
This matters more than it sounds. Most bugs in code that handles lists come from a mutating method being called on an array that belonged to somebody else, and this lesson is really about learning to notice which one you are holding.
Adding and removing at the ends
Explanation
Four methods work on the ends of an array. `push(value)` adds to the end and `pop()` removes from the end. `unshift(value)` adds to the start and `shift()` removes from the start. All four change the array in place — after `list.push(4)`, `list` itself is longer.
The two that remove hand you back the element they removed, which is often the whole point: `const next = queue.shift();` both takes the first person out of the queue and tells you who it was. On an empty array both `pop()` and `shift()` return `undefined` rather than throwing.
The two that add hand back the array's new length. That is a much less useful answer, and it is the source of the mistake in the next section.
push does not give you the array back
A common mistake
`const bigger = list.push('c');` does not put an array in `bigger`. It puts a number there — the new length. The array that changed is `list`, which you already had.
This trips people up because `push` reads like it should produce the longer list. It does not, and the resulting bug is quiet: `bigger` is a number, so the next line that treats it as an array fails somewhere else entirely.
The rule to carry: if a method's job is to change the array, do not expect the array as its answer. Call it as a statement on its own line, and keep using the original name.
slice takes a copy
Explanation
`slice(start, end)` returns a new array containing the elements from `start` up to but not including `end`. `[10, 20, 30, 40].slice(1, 3)` gives `[20, 30]` — index 1 and index 2, stopping before index 3. The original is untouched.
The end being excluded feels arbitrary until you notice it makes the arithmetic pleasant: the number of elements you get is simply `end - start`, and `slice(0, 2)` reads as the first two.
Two shorthands are worth having. `slice()` with no arguments copies the whole array — the standard way to get an array you are allowed to damage. And a negative number counts back from the end, so `slice(0, -1)` means everything except the last element.
splice is not slice
A common mistake
`splice` is spelled one letter away from `slice` and does close to the opposite thing. `list.splice(start, count)` removes `count` elements starting at `start`, changes `list` in place, and returns an array of the elements it removed.
So `slice(1, 3)` gives you a copy of two elements and leaves the array alone, while `splice(1, 3)` rips three elements out of the array and gives you those. Reading code quickly and seeing the wrong one is a genuinely common mistake, and the symptom — the caller's data quietly shrinking — is hard to trace back.
`splice` is not a bad method. It is the clearest way to remove an element from the middle, and if the array is one you built yourself a moment ago, mutating it is fine. The danger is only ever about whose array it is.
Building a new array instead
Why it matters
When a function is handed an array and returns a changed version, the caller almost never expects their own array to have moved. `concat` is the copying way to add: `base.concat('c')` returns a new array with `'c'` on the end and leaves `base` exactly as it was.
The habit that follows is worth adopting now, because the rest of this module builds on it: copy first, then change the copy, then return the copy. It costs a little memory and buys you functions that are safe to call from anywhere.
Lesson 5 introduces a second, shorter way to write the same thing using `...`, and Lesson 6 explains precisely what a copy does and does not protect. For now, `slice` and `concat` are enough.
Recap
Recap
push, pop, shift, unshift and splice change the array you call them on. slice and concat build a new one. The removers give you the removed element, the adders give you the new length, and slice's end index is excluded. When the array came from somewhere else, copy before you change it.
Worked examples
Every output below was produced by actually running the code in the same sandbox your exercises run in — it is not a prediction.
push, pop, unshift and shift
Run all four end-of-array methods on one array, printing both what each call returns and what the array looks like afterwards.
1const queue = ["ann", "bo"];2 3console.log(queue.push("cy"));4console.log(queue);5console.log(queue.pop());6console.log(queue);7console.log(queue.unshift("zed"));8console.log(queue);9console.log(queue.shift());10console.log(queue);- line 3
push returns the NEW LENGTH, 3 — not the array.
- line 4
The array itself changed. `queue` is the longer list now.
- line 5
pop returns the element it removed, 'cy'.
- line 7
unshift adds at the START and also returns the new length.
- line 9
shift removes from the start and returns 'zed'.
- line 10
Four calls later, `queue` is back where it began.
What it prints
3["ann","bo","cy"]cy["ann","bo"]3["zed","ann","bo"]zed["ann","bo"]
slice versus splice, side by side
The same starting array, the same-looking arguments, two completely different outcomes. This is the comparison to remember.
1const original = [10, 20, 30, 40, 50];2 3const middle = original.slice(1, 3);4console.log(middle);5console.log(original);6 7const removed = original.splice(1, 3);8console.log(removed);9console.log(original);- line 3
slice(1, 3) copies index 1 and index 2 — the end index is excluded.
- line 5
The original still has all five elements. slice cannot change it.
- line 7
splice(1, 3) removes three elements starting at index 1.
- line 8
What comes back is what was removed, not what is left.
- line 9
The original is now two elements long. This is the surprise.
What it prints
[20,30][10,20,30,40,50][20,30,40][10,50]
Copying without changing the original
concat and slice both hand back a new array. The last line shows that the copy really is a different array, which Lesson 6 makes a great deal of.
1const base = ["a", "b"];2const joined = base.concat("c");3const copy = base.slice();4 5console.log(joined);6console.log(base);7console.log(copy);8console.log(copy === base);- line 2
concat builds a new array; `base` is not consulted about it.
- line 3
slice() with no arguments is the standard whole-array copy.
- line 5
`base` is untouched — this is the difference from push.
- line 7
Same contents, but === is false: they are two separate arrays. Lesson 6 explains why.
What it prints
["a","b","c"]["a","b"]["a","b"]false
Check yourself
Not scored, not stored. Getting one wrong is the useful part.
Given `const list = ['a']; const result = list.push('b');` — what is `result`?
What does this print?
const nums = [1, 2, 3, 4];const part = nums.slice(1, 3);console.log(part);console.log(nums);
A caller hands you their array. You must return its first two elements without changing their array. Which do you use?
What does this print?
const empty = [];console.log(empty.pop());console.log(empty.length);
Write it yourself
Exercise · intro · 8 tests
Add, remove, and one that must not touch anything
Write three functions. `addToEnd(list, value)` adds `value` to the end of `list` and returns the list's new length. `takeFromFront(list)` removes the first element and returns it, or returns undefined for an empty list. `withoutLast(list)` returns a NEW array holding everything except the last element, and must leave `list` exactly as it found it.
addToEnd and takeFromFront are meant to change the caller's array — that is their contract. withoutLast is not: after calling it the caller's array must be unchanged, and withoutLast([]) is an empty array rather than an error.
Define addToEnd, takeFromFront and withoutLast at the top level of your code — the tests call them by name.
Exercise · core · 7 tests
Copy, change the copy, return the copy
Write `withItemReplaced(list, index, value)`, which returns a NEW array identical to `list` except that the element at `index` is `value`, and `withoutItemAt(list, index)`, which returns a NEW array with the element at `index` removed. Neither may change the array it was given. The starter code below returns the right contents and is still wrong — find out why.
Both functions return a different array object from the one passed in, and the caller's array is unchanged afterwards. Removing a position that does not exist returns an unchanged copy rather than throwing.
Define withItemReplaced and withoutItemAt at the top level of your code — the tests call them by name.
Worth remembering
- What does list.push(value) return?
The new length of the array, not the array. The lengthened array is the one you already had, because push mutates in place.
- slice versus splice — which one changes the array?
splice changes it in place and returns the removed elements. slice leaves it alone and returns a new array. slice's end index is excluded.
- You must return a changed version of an array a caller gave you. What is the pattern?
Copy first (list.slice()), change the copy, return the copy. Never mutate an array you did not create.
Check this lesson against the source
We wrote the explanation; we did not invent the facts. This is the page that backs them.
MDN — Array: copying methods and mutating methods
Check there: The section headed Copying methods and mutating methods, which names push, pop, shift, unshift and splice as mutating, and slice and concat among the methods that return a new array.
Check there: That push returns the new length of the array, not the array.
Check there: That end is excluded, and that a negative index counts back from the end.
MDN — Array.prototype.splice()
Check there: That splice changes the array in place and returns the removed elements.
Prefer to read the source in a structured breakdown? Turn this doc into a breakdown