Mastering the Arcane Art of JavaScriptMancy for C# Developers: ES6 Spread Operator

| Comments

The Mastering the Arcane Art of JavaScript-mancy series are my humble attempt at bringing my love for JavaScript to all other C# developers that haven’t yet discovered how awesome this language and its whole ecosystem are. These articles are excerpts of the super duper awesome JavaScript-Mancy book a compendium of all things JavaScript for C# developers.

In More Useful Function Patterns: Multiple Arguments I wrote 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.

Let’s find out how this new ES6 feature can help you write more readable code.

Use the Spread Operator to Seamlessly Concatenate Arrays

You can experiment with all examples in this article directly within this jsBin example

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:

1
2
let knownFoesLevel1 = ['rat', 'rabbit']
let newFoes = ['globin', 'ghoul'];

Since it’s easier to manage one collection than two, we want to merge these two collections within the same array. Where you would have used the concat method in ES5:

1
2
3
let knownFoesLevel2 = knownFoesLevel1.concat(newFoes);
console.log(knownFoesLevel2);
// => ["rat", "rabbit", "globin", "ghoul"]

In ES6 you can use the spread operator to achieve the same effect with a much clearer syntax:

1
2
3
let knownFoesLevel2WithSpread = [...knownFoesLevel1, ...newFoes];
console.log(knownFoesLevel2WithSpread);
// => ["rat", "rabbit", "globin", "ghoul"]

You can even mix arrays and items:

1
2
3
4
5
let undead = ['zombie', 'banshee', 'vampire', 'skeleton'];
let knownFoesLevel3 = [...knownFoesLevel2, 'troll', 'orc', ...undead];
console.log(knownFoesLevel3);
// => ["rat", "rabbit", "globin", "ghoul", "troll", 
//    "orc", "zombie", "banshee", "vampire", "skeleton"]

Easily Apply With the Spread Operator

The spread operator offers you an alternative syntax to Function.prototype.apply.

In The Many JavaScript Quirks you learned how you can use the Function.prototype.apply function to explicitly set the context (this) in which a function is executed. At the same time, you 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 spread let’s you call an arbitrary function with an array of arguments in a much better way than apply.

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

Let’s now 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
2
3
function performActionViciously(agent1, action, agent2){
    console.log(`${agent1} ${action} ${agent2} viciously`);
}

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
2
performActionViciously.apply(/* this */ null, action);
// => hobbit attacks rabbit viciously

Where you would need to fill in the context in which the function will be executed (this) for the apply method to work.

With ES6 you can use the spread operator to easily perform an action:

1
2
3
// let action = ['hobbit', 'attacks', 'rabbit'];
performActionViciously(...action);
// => hobbit attacks rabbit viciously

No need to set the context (this) in which the function is executed and the resulting code is much concise with the omission of apply.

You may be asking yourself, why don’t I make the performActionViciously function take an array as argument? Well, you could do that. But what happens when you have no control over the function being called?

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
2
3
// console.log expects something like this console.log(a1, a2, a3, a4, etc)
console.log.apply(/* this */ console.log, action);
// => 'hobbit', 'attacks', 'rabbit'

With ES6 and the spread operator you can simplify the code sample above as follows:

1
2
console.log(...action);
// => '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
2
3
4
5
let anotherAction = ['jaime', 'cleans', 'the dishes'];
let moreThingsToClean = ['the toilet', 'the hut', 'the stables'];
Array.prototype.push.apply(anotherAction, moreThingsToClean);
console.log(anotherAction);
// => ['jaime', 'cleans', 'the dishes', 'the toilet', 'the hut', 'the stables'];

With the spread operator it would’ve been 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 DOM1 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 input manually (for even wizards can benefit from web interfaces). It could look like this:

1
2
3
4
5
6
7
8
9
10
11
12
<form action="post" id="minion">
    <label for="name">Name:</label>
    <input type="text" name="name" value="Orc" />

    <label for="class">Class:</label>
    <input type="text" name="class" value="Warrior"/>

    <label for="strength">Strength:</label>
    <input type="number" name="strength" value="18"/>

    <button>Save</button>
</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 that will be called when the form is submitted:

1
2
3
4
5
// select the form element with the id of minion
let form = document.querySelector('form#minion');

// when submitting the form we will call the saveMinion function
form.addEventListener('submit', saveMinion);

In the example above we use the document.querySelector method to select the form element that represents the actual form and the addEventListener method to register an 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.

Now, 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 their values.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function saveMinion(e){
    let inputs =  form.querySelectorAll('input'),
        values = [];

    for (let i = 0; i < inputs.length; i++) {
      values.push(inputs[i].value);
    }

    console.log(values);
    // => ["Orc", "Warrior", "18"]

    // TODO: createMinion(values);

    // this just prevents the form from being submitted via AJAX
    e.preventDefault();
}

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 an 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 do this? 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
2
3
4
5
6
7
8
9
10
11
12
function saveMinionWithSpread(e){
    let values = [...form.querySelectorAll('input')]
      .map(i => i.value);

    console.log(values);
    // => ["Orc", "Warrior", "18"]

    // TODO: createMinion(values);

    // this just prevents the form from being submitted via AJAX
    e.preventDefault();
}

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.

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
2
3
4
5
6
7
8
// You can also convert any iterable into an array using spread
let exits = new Set(['north', 'south', 'east', 'west']);

console.log(exits);
// => [object Set]

console.log([...exits]);
// => ['north', 'south', 'east', 'west];

Or a Map (like a C# Dictionary):

1
2
3
4
5
6
7
8
9
10
let box = new Map();
box.set('jewels', ['emerald', 'ruby']);
box.set('gold coins', 100);

console.log(box);
// => [object Map]

console.log([...box])
// => [["jewels", ["emerald", "ruby"]], ["gold coins", 100]]
// 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 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. The next natural step would be to create a new minion using those characteristics and the following constructor function:

1
2
3
4
5
6
7
8
function Minion(name, minionClass, strength){
  this.name = name;
  this.minionClass = minionClass;
  this.strength = strength;
  this.toString = function(){
    return `I am ${name} and I am a ${minionClass}`;
  }
}

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 a more readable code:

1
let newMinion = new Minion(...values);

The full code example could look like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// add event handler for the form submit event
form.addEventListener('submit', saveMinionForReal);

function saveMinionForReal(e){
    let values = [... form.querySelectorAll('input')]
      .map(i => i.value);
    console.log(values);
    // => ["Orc", "Warrior", "18"]

    // create minion with the values
    let newMinion = new Minion(...values);
    console.log(`Raise and live my minion: ${newMinion}!!!`)
    // => Raise and live my minion: I am Orc and I am a Warrior!!!

    // saveNewMinion(newMinion);

    e.preventDefault();
}

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.

Concluding

In this article 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 a 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.

Interested In Learning More JavaScript? Buy the Book!

Are you a C# Developer interested in learning JavaScript? Then take a look at the JavaScript-mancy book, a complete compendium of JavaScript for C# developers.

a JavaScriptmancy sample cover

Have a awesome week!

More Articles in These Series


  1. The DOM or Document Object Model is an object representation of a website where each HTML element is represented by an object called a node.

Comments