Immutable Array Methods: How to Write Remarkably Clean JavaScript Code

Share this article

Immutable Array Methods: How to Write Remarkably Clean JavaScript Code
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.

FAQs on Creating and Using Immutable Array Methods in JavaScript

What is the concept of immutability in JavaScript?

Immutability is a fundamental concept in programming that refers to the state of an object which cannot be modified after it’s created. In JavaScript, immutability is not enforced by default. However, it’s a powerful concept that can help you write more predictable and easier to maintain code. It’s particularly useful in functional programming where immutability can prevent bugs and complexities that come from changing state.

Why should I use immutable array methods in JavaScript?

Using immutable array methods in JavaScript can lead to cleaner, more maintainable code. Since these methods do not modify the original array, they prevent side effects that can lead to bugs. This is particularly important in large codebases or when working with complex data structures. Immutable methods return a new array, leaving the original array untouched. This makes your code more predictable and easier to debug.

What are some examples of immutable array methods in JavaScript?

JavaScript provides several immutable array methods that do not change the original array. Some examples include the ‘map()’, ‘filter()’, ‘reduce()’, and ‘concat()’ methods. The ‘map()’ method creates a new array with the results of calling a provided function on every element in the array. The ‘filter()’ method creates a new array with all elements that pass a test implemented by the provided function. The ‘reduce()’ method applies a function against an accumulator and each element in the array to reduce it to a single output value. The ‘concat()’ method is used to merge two or more arrays and returns a new array.

How can I make my arrays immutable in JavaScript?

JavaScript does not provide a built-in way to make arrays immutable. However, you can achieve immutability by using methods that do not mutate the original array, such as ‘map()’, ‘filter()’, ‘reduce()’, and ‘concat()’. Another approach is to use the Object.freeze() method, which prevents new properties from being added to an object, existing properties from being removed, and prevents changing the enumerability, configurability, or writability of existing properties.

What is the difference between mutable and immutable methods in JavaScript?

The main difference between mutable and immutable methods in JavaScript lies in how they treat the original array. Mutable methods modify the original array, while immutable methods do not. Instead, immutable methods return a new array. This makes your code more predictable and easier to debug, as it prevents side effects that can lead to bugs.

Can I use immutable arrays with other data types in JavaScript?

Yes, you can use immutable arrays with other data types in JavaScript. The concept of immutability applies to all data types, not just arrays. For example, you can use immutable methods with strings, numbers, objects, and more. This can help you write cleaner, more maintainable code.

Are there any performance implications when using immutable array methods?

Using immutable array methods can have some performance implications, as they often create a new array instead of modifying the original one. This can lead to increased memory usage, especially with large arrays. However, in most cases, the benefits of using immutable methods, such as cleaner code and fewer bugs, outweigh the potential performance costs.

How can I use the ‘reduce()’ method in JavaScript?

The ‘reduce()’ method in JavaScript is an immutable method that applies a function against an accumulator and each element in the array to reduce it to a single output value. Here’s an example of how to use it:

const array = [1, 2, 3, 4];
const sum = array.reduce((accumulator, currentValue) => accumulator + currentValue, 0);
console.log(sum); // Outputs: 10

In this example, the ‘reduce()’ method calculates the sum of all elements in the array.

What is the ‘concat()’ method in JavaScript?

The ‘concat()’ method in JavaScript is used to merge two or more arrays. This method does not change the existing arrays but instead returns a new array. Here’s an example:

const array1 = ['a', 'b', 'c'];
const array2 = ['d', 'e', 'f'];
const array3 = array1.concat(array2);
console.log(array3); // Outputs: ["a", "b", "c", "d", "e", "f"]

In this example, the ‘concat()’ method merges ‘array1’ and ‘array2’ into a new array ‘array3’.

How can I use the ‘filter()’ method in JavaScript?

The ‘filter()’ method in JavaScript creates a new array with all elements that pass a test implemented by the provided function. Here’s an example of how to use it:

const array = [1, 2, 3, 4, 5];
const filtered = array.filter(num => num > 3);
console.log(filtered); // Outputs: [4, 5]

In this example, the ‘filter()’ method creates a new array with the numbers that are greater than 3.

Darren JonesDarren Jones
View Author

Darren loves building web apps and coding in JavaScript, Haskell and Ruby. He is the author of Learn to Code using JavaScript, JavaScript: Novice to Ninja and Jump Start Sinatra.He is also the creator of Nanny State, a tiny alternative to React. He can be found on Twitter @daz4126.

array functionsarray mutationmutation
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week