Here’s an example showing how it can potentially cause some problems:
const numbers = [1,2,3]; const countdown = numbers.reverse();
This code looks fine. We have an array called
numbers, and we want another array called
countdown that lists the numbers in reverse order. And it seems to work. If you check the value of the
countdown variable, it’s what we expect:
The unfortunate side effect of the operation is that the
reverse() method has mutated the
numbers array as well. This is not what we wanted at all:
Even worse, the two variables both reference the same array, so any changes that we subsequently make to one will affect the other. Suppose we use the
Array.prototype.push() method to add a value of
0 to the end of the
countdown array. It will do the same to the
numbers array (because they’re both referencing the same array):
countdown.push(0) << 4 countdown << [3,2,1,0] numbers << [3,2,1,0]
It’s this sort of side effect that can go unnoticed — especially in a large application — and cause some very hard-to-track bugs.
reverse isn’t the only array method that causes this sort of mutation mischief. Here’s a list of array methods that mutate the array they’re called on:
Slightly confusingly, arrays also have some methods that don’t mutate the original array, but return a new array instead:
These methods will return a new array based on the operation they’ve carried out. For example, the
map() method can be used to double all the numbers in an array:
const numbers = [1,2,3]; const evens = numbers.map(number => number * 2); << [2,4,6]
Now, if we check the
numbers array, we can see that it hasn’t been affected by calling the method:
There doesn’t seem to be any reason for why some methods mutate the array and others don’t. But the trend with recent additions is to make them non-mutating. It can be hard to remember which do which.
Ruby has a nice solution to this in the way it uses bang notation. Any method that causes a permanent change to the object calling it ends in a bang.
[1,2,3].reverse! will reverse the array, while
[1,2,3].reverse will return a new array with the elements reversed.
Immutable Array Methods: Let’s Fix this Mutating Mess!
We’ve established that mutations can be potentially bad and that a lot of array methods cause them. Let’s look at how we can avoid using them.
It’s not so hard to write some functions that return a new array object instead of mutating the original array. These functions are our immutable array methods.
Because we’re not going to monkey patch
Array.prototype, these functions will always accept the array itself as the first parameter.
Let’s start by writing a new
pop function that returns a copy of the original array but without the last item. Note that
Array.prototype.pop() returns the value that was popped from the end of the array:
const pop = array => array.slice(0,-1);
This function uses
Array.prototype.slice() to return a copy of the array, but with the last item removed. The second argument of -1 means
stop slicing 1 place before the end. We can see how this works in the example below:
const food = ['🍏','🍌','🥕','🍩']; pop(food) << ['🍏','🍌','🥕']
Next, let’s create a
push() function that will return a new array, but with a new element appended to the end:
const push = (array, value) => [...array,value];
This uses the spread operator to create a copy of the array. It then adds the value provided as the second argument to the end of the new array. Here’s an example:
const food = ['🍏','🍌','🥕','🍩']; push(food,'🍆') << ['🍏','🍌','🥕','🍩','🍆']
Shift and Unshift
We can write replacements for
const shift = array => array.slice(1);
shift() function, we’re just slicing off the first element from the array instead of the last. This can be seen in the example below:
const food = ['🍏','🍌','🥕','🍩']; shift(food) << ['🍌','🥕','🍩']
unshift() method will return a new array with a new value appended to the beginning of the array:
const unshift = (array,value) => [value,...array];
The spread operator allows us to place values inside an array in any order. We simply place the new value before the copy of the original array. We can see how it works in the example below:
const food = ['🍏','🍌','🥕','🍩']; unshift(food,'🍆') << ['🍆','🍏','🍌','🥕','🍩']
Now let’s have a go at writing a replacement for the
Array.prototype.reverse() method. It will return a copy of the array in reverse order, instead of mutating the original array:
const reverse = array => [...array].reverse();
This method still uses the
Array.prototype.reverse() method, but applies to a copy of the original array that we make using the spread operator. There’s nothing wrong with mutating an object immediately after it has been created, which is what we’re doing here. We can see it works in the example below:
const food = ['🍏','🍌','🥕','🍩']; reverse(food) << ['🍩','🥕','🍌','🍏']
Finally, let’s deal with
Array.prototype.splice(). This is a very generic function, so we won’t be completely rewriting what it does (although that would be an interesting exercise to try. (Hint: use the spread operator and
splice().) Instead, we’ll focus on the two main uses for slice: removing items from an array and inserting items into an array.
Removing an Array Item
Let’s start with a function that will return a new array, but with an item at a given index removed:
const remove = (array, index) => [...array.slice(0, index),...array.slice(index + 1)];
Array.prototype.slice() to slice the array into two halves — either side of the item we want to remove. The first slice returns a new array, copying the original array’s elements until the index before the one specified as an argument. The second slice returns an array with the elements after the one we’re removing, all the way to the end of the original array. Then we put them both together inside a new array using the spread operator.
We can check this works by trying to remove the item at index 2 in the
food array below:
const food = ['🍏','🍌','🥕','🍩']; remove(food,2) << ['🍏','🍌','🍩']
Adding an Array Item
Finally, let’s write a function that will return a new array with a new value inserted at a specific index:
const insert = (array,index,value) => [...array.slice(0, index), value, ...array.slice(index)];
This works in a similar way to the
remove() function. It creates two slices of the array, but this time includes the element at the index provided. When we put the two slices back together, we insert the value provided as an argument between them both.
We can check this works by trying to insert a cupcake emoji into the middle of our
const food = ['🍏','🍌','🥕','🍩'] insert(food,2,'🧁') << ['🍏','🍌','🧁','🥕','🍩']
Now we have a set of immutable array methods that leave our original arrays alone. I’ve saved them all in one place on CodePen, so feel free to copy them and use them in your projects. You could namespace them by making them methods of a single object or just use them as they are when required.
These should enough for most array operations. If you need to perform a different operation, remember the golden rule: make a copy of the original array using the spread operator first. Then, immediately apply any mutating methods to this copy.
Are there any other array methods you can think of that would benefit from having an immutable version? Why not reach out on Twitter to let me know.