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();
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]
reverse()
method has mutated the numbers
array as well. This is not what we wanted at all:
numbers
<< [3,2,1]
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
Andreverse
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:
- Array.prototype.pop()
- Array.prototype.push()
- Array.prototype.shift()
- Array.prototype.unshift()
- Array.prototype.reverse()
- Array.prototype.sort()
- Array.prototype.splice()
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]
numbers
array, we can see that it hasn’t been affected by calling the method:
numbers
<< [1,2,3]
[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 patchArray.prototype
, these functions will always accept the array itself as the first parameter.
Pop
Let’s start by writing a newpop
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);
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 apush()
function that will return a new array, but with a new element appended to the end:
const push = (array, value) => [...array,value];
const food = ['🍏','🍌','🥕','🍩'];
push(food,'🍆')
<< ['🍏','🍌','🥕','🍩','🍆']
Shift and Unshift
We can write replacements forArray.prototype.shift()
and Array.prototype.unshift()
similarly:
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];
const food = ['🍏','🍌','🥕','🍩'];
unshift(food,'🍆')
<< ['🍆','🍏','🍌','🥕','🍩']
Reverse
Now let’s have a go at writing a replacement for theArray.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();
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 withArray.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)];
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,'🧁')
<< ['🍏','🍌','🧁','🥕','🍩']
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 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.