Immutable Array Methods: How to Write Remarkably Clean JavaScript Code

    Darren Jones
    Share

    In our guide to variable assignment and mutation in JavaScript, we looked at issues with variable mutations and how to manage them. In this article, we’ll see how array methods that mutate the original array make life difficult for us. But it’s not all doom and gloom. We’ll write immutable array methods that fix these issues — and you’ll be able to start using them in your code today.

    Explore this topic in greater detail, and get up to speed with modern JavaScript, in my new book Learn to Code with JavaScript.

    Array Mutations in JavaScript

    Arrays in JavaScript are just objects, which means they can be mutated. In fact, many of the built-in array methods will mutate the array itself. This can mean the golden rule from above gets broken, just by using one of the built-in methods.

    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:

    countdown
    << [3,2,1]

    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:

    numbers
    << [3,2,1]

    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.

    Mutable Array Methods in JavaScript

    And 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:

    numbers
    << [1,2,3]

    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.

    Pop

    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)
    << ['🍏','🍌','🥕']

    Push

    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 Array.prototype.shift() and Array.prototype.unshift() similarly:

    const shift = array => array.slice(1);

    For our 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)
    << ['🍌','🥕','🍩']

    Our 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,'🍆')
    << ['🍆','🍏','🍌','🥕','🍩']

    Reverse

    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)
    << ['🍩','🥕','🍌','🍏']

    Splice

    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)];

    This uses 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 food array:

    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.

    Conclusion

    In this article, we looked at how JavaScript makes life difficult with array methods that mutate the original array as part of the language. Then we wrote our own immutable array methods to replace these functions.

    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.

    Don’t forget to check out my new book Learn to Code with JavaScript if you want to get up to speed with modern JavaScript.