Upgrading Your Everyday JavaScript Magic With ES6 - The Spread Operator
Learn to recognize beauty in code,
it will make the task of coding
a pleasure in itself,
it will make you appreciate code
in a whole different way.
- Zazongel emjia
Bard and Poet
Ready To Spread Your Wings?
In More Useful Function Patterns: Multiple Arguments you learned about rest parameters, a new ES6 feature, that lets you define functions with an arbitrary number of arguments just like params in C#.
The spread operator works sort of in an opposite way to the rest operator. Where the rest operator takes a variable number of arguments and packs them into an array, the spread operator takes and array and expands it into its compounding items.
In this chapter you’ll see several recipes that will help you write better code by taking advantage of the spread operator like, for instance, concatenating arrays.
Use the Spread Operator to Seamlessly Concatenate Arrays
You can use the spread operator to easily concatenate arrays with each other. Let’s say that we want to collect our most terrible enemies for later reference. We have an array knownFoesLevel1 and another array newFoes with newly acquired enemies (because you can never have enough enemies):
1 let knownFoesLevel1 = ['rat', 'rabbit']
2 let newFoes = ['globin', 'ghoul'];
Since it’s easier to manage one collection than two, we want to merge these two collections into one single array. Where you would have used the concat method in ES5:
1 let knownFoesLevel2 = knownFoesLevel1.concat(newFoes);
2 console.log(knownFoesLevel2);
3 // => ["rat", "rabbit", "globin", "ghoul"]
In ES6 you can use the spread operator to achieve the same result with a much clearer syntax:
1 let knownFoesLevel2WithSpread = [...knownFoesLevel1, ...newFoes];
2 console.log(knownFoesLevel2WithSpread);
3 // => ["rat", "rabbit", "globin", "ghoul"]
You can even mix arrays and singular items:
1 let undead = ['zombie', 'banshee', 'vampire', 'skeleton'];
2 let knownFoesLevel3 = [...knownFoesLevel2, 'troll', 'orc',
3 ...undead];
4 console.log(knownFoesLevel3);
5 // => ["rat", "rabbit", "globin", "ghoul", "troll",
6 // "orc", "zombie", "banshee", "vampire", "skeleton"]
Easily Use Apply With the Spread Operator
Another useful use case of the spread operator is as an alternative syntax to Function.prototype.apply.
In Mysteries of the JavaScript Arcana you learned about how you can use the apply function to explicitly set the context (this) in which a function is executed. You also learned how apply expects an array of arguments as second parameter and how when the function is finally invoked each element within the array is passed as a separate argument to the original function.
Well the spread operator let’s you call an arbitrary function with an array of arguments in a better way than apply does.
Let’s say that you are working on a spell to command your minions with random actions because being too predictive is boring and you appreciate the wild factor. You express these random actions as arrays: ['minion1', 'action', 'minion2']
1 let action = ['hobbit', 'attacks', 'rabbit'];
Now let’s say that you have a function of your own device where you want actions to be done viciously (looks like you are in a foul mood today):
1 function performActionViciously(agent1, action, agent2){
2 console.log(`${agent1} ${action} ${agent2} viciously`);
3 }
Because the action is expressed as an array but the performActionViciously function expects a separate series of arguments you need a way to adapt these two disparate elements.
Prior to ES6 you would have used the apply function:
1 performActionViciously.apply(/* this */ null, action);
2 // => hobbit attacks rabbit viciously
Where you would need to fill in the context in which the function will be executed for the apply method to work (that is, the value of this).
With ES6 you can use the spread operator to easily perform an action:
1 // let action = ['hobbit', 'attacks', 'rabbit'];
2 performActionViciously(...action);
3 // => hobbit attacks rabbit viciously
No need to set the context in which the function is executed and the resulting code is much concise with the omission of apply.
Now you may be asking yourself: Why don’t I make the performActionViciously function take an array as argument and forget all this spread operator nonsense? Well, you could do that. But what happens when you have no control over the function being called?
Take console.log. Imagine that, instead of performing these actions viciously, you just want to log them. Because console.log takes an arbitrary number of arguments and you have an array, you need some way to adapt the array to the expected signature. Again, prior to ES6 you would use apply:
1 // console.log expects something like this
2 // console.log(a1, a2, a3, a4, etc)
3 console.log.apply(/* this */ console.log, action);
4 // => 'hobbit', 'attacks', 'rabbit'
With ES6 and the spread operator you can simplify the code sample above greatly:
1 console.log(...action);
2 // => 'hobbit', 'attacks', 'rabbit'
Another example in which the spread operator comes handy is when we want to extend an existing array with another array. In the olden days we would have written:
1 let anotherAction = ['jaime', 'cleans', 'the dishes'];
2 let moreThingsToClean = ['the toilet', 'the hut', 'the stables'];
3 Array.prototype.push.apply(anotherAction, moreThingsToClean);
4 console.log(anotherAction);
5 // => ['jaime', 'cleans', 'the dishes', 'the toilet',
6 // 'the hut', 'the stables'];
With the spread operator it’s as easy as:
1 anotherAction.push(...moreThingsToClean);
In summary, do you have some variable as an array and need to apply it to a function that takes separate arguments? Use the spread operator.
Converting Array-likes and Collections Into Arrays
Another interesting application of the spread operator is to convert array-like objects into arrays.
If you remember More Useful Function Patterns: Multiple Arguments, array-like objects are a special type of object that can be indexed, enumerated, has a length property but doesn’t have any of the methods of an array. Some examples of array-like objects are the arguments object inside functions or the list of DOM23 nodes that result when using document.querySelector.
Let’s imagine that we have a web-based user interface, a form, to help us create minions based on some characteristics that we can type manually (for even wizards can benefit from web interfaces). It could look like this:
1 <form action="post" id="minion">
2 <label for="name">Name:</label>
3 <input type="text" name="name" value="Orc">
4
5 <label for="class">Class:</label>
6 <input type="text" name="class" value="Warrior">
7
8 <label for="strength">Strength:</label>
9 <input type="number" name="strength" value="18">
10
11 <button>Save</button>
12 </form>
When you click on the Save button we want to store these values and create a new minion that will serve us for eternity. So we add an event handler saveMinion that will be called when the form is submitted:
1 // select the form element with the id of minion
2 let form = document.querySelector('form#minion');
3
4 // when submitting the form we will call the saveMinion function
5 form.addEventListener('submit', saveMinion);
In the example above we use the document.querySelector method to select the form element that represents the actual form on the web page. After that, we call the addEventListener method to register a saveMinion event handler for the submit event of the form. Whenever the user clicks on the Save button, the form will be submitted and the saveMinion method will be called.
The next step would be to extract the values from the inputs above. How can we go about that? Well, we can select all the inputs within the form and extract the values that we or another wizards have typed in.
1 function saveMinion(e){
2 let inputs = form.querySelectorAll('input'),
3 values = [];
4
5 for (let i = 0; i < inputs.length; i++) {
6 values.push(inputs[i].value);
7 }
8
9 console.log(values);
10 // => ["Orc", "Warrior", "18"]
11
12 // TODO: createMinion(values);
13
14 // this just prevents the form from being submitted via AJAX
15 e.preventDefault();
16 }
So we use the form.querySelectorAll('input') method to select all input elements within the form. This method returns an array-like object of nodes. Because it has a length property we can use a simple for loop and a new array values to collect the values. After that we can create our brand new minion with the extracted values.
But, is there a better way to collect these values? What about converting the inputs array-like object to an array and using the helpful array methods instead of the for loop? Spread operator to the rescue!
1 function saveMinionWithSpread(e){
2 let values = [...form.querySelectorAll('input')]
3 .map(i => i.value);
4
5 console.log(values);
6 // => ["Orc", "Warrior", "18"]
7
8 // TODO: createMinion(values);
9
10 // this just prevents the form from being submitted via AJAX
11 e.preventDefault();
12 }
By converting the array-like to an array using the spread operator we can use array functions such as map and write more beautiful code! map works just like LINQ’s Select and let’s you perform transformations on each item of a collection. In this case we just transform a collection of elements into values. Awesome right?
In addition to array-like objects you can use the spread operator to convert any iterable object to an array. For instance a Set (a collection of unique items):
1 // You can also convert any iterable into an array using spread
2 let exits = new Set(['north', 'south', 'east', 'west']);
3
4 console.log(exits);
5 // => [object Set]
6
7 console.log([...exits]);
8 // => ['north', 'south', 'east', 'west];
Or a Map (like a C# Dictionary):
1 let box = new Map();
2 box.set('jewels', ['emerald', 'ruby']);
3 box.set('gold coins', 100);
4
5 console.log(box);
6 // => [object Map]
7
8 console.log([...box])
9 // => [["jewels", ["emerald", "ruby"]], ["gold coins", 100]]
10 // in this case we get an array of key-value pairs
Spread Lets You Combine New and Apply
The spread operator also lets you combine the new operator with the ability to apply arguments to a function. That is, the ability to instantiate objects using a constructor function while, at the same time, adapting an array of arguments into a constructor function that expects separate arguments.
Let’s continue the example from the previous section where we extracted the characteristics of our minion from an HTML form. To refresh your memory the form looked like this:
1 <form action="post" id="minion">
2 <label for="name">Name:</label>
3 <input type="text" name="name" value="Orc">
4
5 <label for="class">Class:</label>
6 <input type="text" name="class" value="Warrior">
7
8 <label for="strength">Strength:</label>
9 <input type="number" name="strength" value="18">
10
11 <button>Save</button>
12 </form>
The next natural step would be to create a new minion using those characteristics and the following constructor function:
1 function Minion(name, minionClass, strength){
2 this.name = name;
3 this.minionClass = minionClass;
4 this.strength = strength;
5 this.toString = function(){
6 return `I am ${name} and I am a ${minionClass}`;
7 }
8 }
If we were to use pure ES5 we would need to unwrap the values before we use them:
1 var newMinion = new Minion(values[0], values[1], values[2]);
With ES6 we can combine new with the spread operator to get this beautiful piece of code:
1 let newMinion = new Minion(...values);
The full code example could look like this:
1 // add event handler for the form submit event
2 form.addEventListener('submit', saveMinionForReal);
3
4 function saveMinionForReal(e){
5 let values = [... form.querySelectorAll('input')]
6 .map(i => i.value);
7 console.log(values);
8 // => ["Orc", "Warrior", "18"]
9
10 // create minion with the values
11 let newMinion = new Minion(...values);
12 console.log(`Raise and live my minion: ${newMinion}!!!`)
13 // => Raise and live my minion: I am Orc and I am a Warrior!!!
14
15 // saveNewMinion(newMinion);
16
17 e.preventDefault();
18 }
In the example above we first extract the values from the form and then we use them to create a newMinion object by applying both the new and the spread operators at once.
Concluding
In this chapter you learned about the ES6 spread operator and how it works in sort of the opposite way to the rest operator. Instead of grouping separate items into an array, the spread operator expands arrays into separate items.
You learned how you can use it in many scenarios usually resulting in more readable code: to easily concatenate arrays, as a substitute for apply, to convert array-like objects and even other iterables to arrays and, finally, to combine the new operator with apply.
This chapter wraps the first tome of JavaScriptmancy! Great job JavaScriptmancer! Time to spread your wings and dive into the mysteries of data structures in JavaScript!