The spread operator, …, was first introduced in ES6. It quickly became one of the most popular features. So much so that despite the fact it only worked on arrays, a proposal was made to extend its functionalities to objects. This feature was finally introduced in ES9.
The goal of this tutorial, which is divided into two parts, is to show you why the spread operator should be used, how it works, and to deep dive into its uses, from the most basic to the most advanced. If you haven't read the first part of this tutorial, I encourage you to do so! Here is the link:
Here is a short summary of the contents of this tutorial:
- Why the spread operator should be used
- Cloning arrays/objects
- Converting array-like structures to array
- The spread operator as an argument
- Adding elements to arrays/objects
- Merging arrays/objects
- Destructuring nested elements
- Adding conditional properties
- Short circuiting
- The rest parameter (…)
- Default destructuring values
- Default properties
In the first part of this article, we learnt about reference data types, accidental variable mutation, and how we could solve this problem by cloning arrays/objects immutably, with the spread operator.
However, there's a slight problem with this approach, when it comes to nested reference data types: The spread operator only performs a shallow clone. What does this mean? If we attempt to clone an object that contains an array, for example, the array inside the cloned object will contain a reference to the memory address where the original array is stored… This means that, while our object is immutable, the array inside it isn't. Here's an example to illustrate this:
As you can see, our squirtleClone has been cloned immutably. When we change the name property of the original pokemon object to 'Charmander', our squirtleClone isn't affected, its name property isn't mutated.
One of the solutions to this problem would be to use the spread operator to clone the nested properties, as shown in the following example:
For obvious reasons, this isn't an ideal approach to solving our problem. We would need to use the spread operator for every single reference type property, which is why this approach is only valid for small objects. So, which is the optimal solution? Deep cloning.
Since there is plenty to say about deep cloning, I won't be going into too much detail. I'd just like to say that the correct of deep cloning is either using an external library (for example, Lodash), or writing ourselves a function that does it.
Sometimes we need to add properties to an object, but we don't know whether or not those properties exist. This doesn't pose much of a problem, we can always check if the property exists with an if statement:
There is, however, a much simpler way of achieving the same result, by using short circuiting conditionals with the && operator. A brief explanation:
Let's take a look at the following code:
If starterPokemon.length > 0 is false (the array is empty), the statement will short circuit, and our choosePokemon function will never be executed. This is why the previous code is equivalent to using the traditional if statement.
Going back to our original problem, we can take advantage of the logical AND operator to add conditional properties to an object. Here's how:
What's going on here? Allow me to explain:
As we already know, by using the && operator, the second part of the statement will only be executed if the first operand is true. Therefore, only if the abilities variable is true (if the variable exists), will the second half of the statement be executed. What does this second half do? It creates an object containing the abilities variable, which is then destructured with the spread operator placed in front of the statement, thus adding the existent abilities variable into our fullPokemon object immutably.
Before we can introduce our final advanced spreading use, adding default properties to objects, we must first dive into two new concepts: default destructuring values, and the rest parameter. Once we are familiar with these techniques, we will be able to combine them to add default properties to objects.
If we try to destructure an array element or object property that doesn't exist, we will get an undefined variable. How can we avoid undefined values? By using defaults. How does this work?
We can assign default values to the variables we destructure, inside the actual destructuring statement. Here's an example:
As you can see, by adding the default value 'Water' to the type variable in the destructuring statement, we avoid an undefined variable in the case of the pokemon object not having the type property.
You may be surprised to hear that the spread operator is overloaded. This means that it has more than one function. It's second function is to act as the rest parameter.
Simply put, the rest operator takes all remaining elements (this is the reason it's named rest, as in the rest of the elements :p ) and places them into an array. Here's an example:
As you can see, we can pass as many abilities as we want to the printPokemon function. Every single value we introduce after the type parameter (the rest of the parameters) will be collected into an array, which we then turn into a string with the join function, and print out.
Note: Remember that the rest parameter must be the last parameter, or an error will occur.
The rest parameter can also be used when destructuring, which is the part that interests us. It allows us to obtain the remaining properties in an object, and store them in an array. Here's an example of the rest parameter used in a destructuring assignment:
As shown above, we can use the rest operator to destructure the remaining properties in the pokemon object. Like in the previous example, our pokemon object can have as many properties as we want defined after the id property, they will all be collected by the rest parameter.
Now that we know how the rest parameter works, and how to apply it in destructuring assignments, let's return to dealing with default properties.
Sometimes, we have a large amount of similar objects, that aren't quite exactly the same. Some of them lack properties that the other objects do have. However, we need all of our objects to have the same properties, simply for the sake of order and coherence. How can we achieve this?
By setting default properties. These are properties with a default value that will be added to our object, if it doesn't already have that property. By using the rest parameter combined with default values and the spread operator, we can add default properties to an object. It may sound a bit daunting, but it's actually quite simple. Here's an example of how to do it:
What's going on in the previous code fragment? Let's break it down:
As you can see, when we destructure the abilities property, we are adding a default value (). As we already know, the default value will only be assigned to the abilities variable if it doesn't exist in the pokemon object. In the same line, we are gathering the remaining properties (name and type) of the pokemon object into a variable named rest, by making use of the awesome rest parameter.
On line 7, we are spreading the rest variable (which, as you can see, is an object containing the name and type properties) inside an object literal, to generate a new object. We are also adding the abilities variable, which in this case, is an empty array, since that is what we specified as its default value on the previous line.
In the case of our original pokemon object already having an abilities property, the previous code would not have modified it, and it would maintain its original value.
So, this is how we add default properties to an object. Let's place the previous code into a function, and apply it to a large collection of objects:
As you can see, all the pokemon in the array now have an abilities property. In the case of charmander and bulbasur, they have an empty array, since that is the default value we assigned. However, the squirtle object maintains its original array of abilities.
There are, of course, other ways of adding default properties to an object, mainly by using if statements. However, I wanted to show an interesting new way of doing it, by using a combination of default values, the rest parameter, and the spread operator. You can then choose which approach suits you best :)
In this second part of the tutorial we have learnt some more advanced uses of the spread operator, which include destructuring nested elements, adding conditional properties and adding default properties. We have also learnt three interesting JS concepts: short-circuiting, default destructuring values and the rest parameter.
I sincerely hope you've found this piece useful, thank you for reading :) If you can think of any more uses of the spread operator or would like to comment something, don't hesitate to reach out, here's a link to my Twitter page.