Javascript is a fascinating language - it's very versatile but can sometimes also be frustrating. In this post, we'll have a look at the spread syntax, another one of those features that was introduced with ES6 and that I don't see being used so often - although it can be pretty potent in the right situations.
The spread operator is used in conjunction with the various types of iterables, and depending on the setting, it can trivialize certain tasks simply by using it alongside some of the other syntactical sugar that exists in JS. For instance, it makes it possible to copy and merge arrays in as little as a single line of code! We'll get to that in a minute.
The spread operator is quite fascinating, it's usefulness isn't immediately evident since it can be employed in many places that you wouldn't expect or even think about - we'll tackle some of those syntactical intricacies as well throughout the sections.
let brain = [...ideas]
The JS Spread operator
As it's name suggests, the spread operator in javascript allows us to expand the elements of an iterable into places where multiple elements are expected. This is done via the spread syntax - the spread operator being represented by three consecutive dots, preceding an iterable: ...nameOfIterable
. Iterables in javascript come in many forms, such as arrays, strings and objects to name a few.
Let's have a look at an example, assume we have the following array of numbers:
let arr = [1,2,3,4,5]
console.log(arr) // prints [1, 2, 3, 4, 5]
Here we're simply printing the entire array to the console. Now what if we preceded this array by the spread operator?
console.log(...arr) // prints 1 2 3 4 5
What happened here? Why did it print the numbers individually and not as an array?
The console.log() function is a bit special in that it accepts an arbitrary number of input parameters. Using the spread operator we're essentially passing the contents of the array as individual inputs to the console.log() function - we're spreading the array elements over the function's input parameters.
console.log(...arr) // prints 1 2 3 4 5
console.log(1, 2, 3, 4, 5) // the same effect
On a first encounter the spread syntax might seem a little bit odd, since it's an unusual symbol, as opposed to other operators that might be more familiar. And at first glance it might not seem very useful, but we'll cover some really interesting use-cases throughout the upcoming sections.
Before we move on - I just want to showcase a super cool use-case: we can quickly find the maximum or minimum numbers in an array by using the spread syntax alongside the Math.max()
and Math.min()
methods:
let arr = [100, 5, 201, 200, 2]
let maxNum = Math.max(...arr) // 201
let minNum = Math.min(...arr) // 2
Cloning Iterables
How do you make a copy of an array in Javascript? The most straightforward method is probably via the good old for loop:
let arr = [1, 2, 3, 4, 5]
let copyOfArr = []
for(let i = 0; i < arr.length; i++){
copyOfArr.push(arr[i])
}
If you're feeling fancy, you can alternatively also use the map() array method:
let copyOfArr = arr.map(x => x)
I love for loops, but they're a bit annoying to write out sometimes. Using the spread operator is probably the shortest syntax to make a copy - look how neat the code is in this case:
let arr = [1, 2, 3, 4, 5]
// spreading arr into an array literal
let arr2 = [...arr]
Just as a heads up - when dealing with nested arrays, the spread syntax will only create a shallow copy of the array - have a look at this example:
let nestedArr = [[1],[2]]
let copyOfNestedArr = [...nestedArr]
// adding to an inner array modifies both copies
copyOfNestedArr[0].push(3)
console.log(nestedArr, copyOfNestedArr) // [[1,3],[2]] and [[1,3],[2]]
// adding to the top-level array only changes one
copyOfNestedArr.push(1)
console.log(nestedArr, copyOfNestedArr) // [[1,3],[2]] and [[1,3],[2],1]
Just to explain this really quickly - in programming, the term deep copy usually refers to the act of cloning an object, such that the new object is it's own independent instance, with it's own independent copy of the data in memory, without reference to the original instance.
A shallow copy on the other hand only immitates the data's structure but points to the same data in memory - hence if a shallow copy were to be modified the original would also change. This is often the case with nested structures, like arrays within arrays and objects within object.
So, keep that in mind!
Additionally, the spread operator might be a little less performant as opposed to a regular for loop. But if you're not copying millions of elements then the difference is most likely negligible.
And as mentioned earlier, copying things this way also works for objects! For example:
Strings to Arrays and Iterables to Objects
Depending on what type of iterable we use the spread operator on, we'll achieve some other effects. Strings for instance are also iterables, the spread operator thus provides an efficient method for quickly turning a string into an array of individual characters:
let str = '12345'
let arrStr = [...str]
Another cool thing that can be done is converting iterables to objects:
let arr = [1, 2, 3]
let obj = {...arr}
console.log(obj) // {0: 1, 1: 2, 2: 3}
let str = `123`
let strObj = {...str}
console.log(strObj) // {0: "1", 1: "2", 2: "3"}
The keys of the resulting object are then simply integers starting from 0.
Merging Arrays and Objects
The spread operator also allows us to concatenate two arrays in a similar manner:
let arr1 = [1, 2, 3]
let arr2 = [4, 5, 6]
let concatenatedArr = [...arr1, ...arr2] // [1, 2, 3, 4, 5, 6]
// we can also insert elements before, after and in between
let customArr = [0, ...arr1, 3.5, ...arr2, 7]
And this also works for objects! We can merge objects like this:
let obj1 = {a: 1, b: 2}
let obj2 = {c: 3, d: 4}
let obj3 = {...obj1, ...obj2}
console.log(obj3) // prints {a: 1, b: 2, c: 3, d: 4}
Careful here though, if the two objects have a property with the same key, then only the last occurence will be considered and added to the resulting merged object.
let obj1 = {a: 1, b: 2}
let obj2 = {b: 3, c: 4}
let obj3 = {...obj1, ...obj2}
console.log(obj3) // prints {a: 1, b: 3, c: 4}
This can however also be a useful behavior! If you want to clone or merge an object, while modifying only a specific property, it can be done as follows:
let obj = {x: 1, y: 2, z: 3}
let modifiedCopy = {...obj, z: 4} // {x: 1, y: 2, z: 4}
Rest Parameters while Destructuring
Destructuring iterables in Javascript is a topic in and of itself, but in a nutshell destructuring happens in the following manner:
let arr = [1, 2, 3]
let [a, b, c] = arr
This is kind of like using the spread operator, however here javascript automatically understands that we want to spread the elements of the array onto the given variables, without actually using the spread syntax. Creating variables in this manner is called a destructuring assignment.
Again, this also works for objects:
let obj = {a: 1, b: 2};
let {a, b} = obj;
console.log(a, b); // prints 1 2
// is equivalent to:
// let a = obj.a;
// let b = obj.b;
Note that in the case of an object the variable names actually need to be the same as the property keys. If you want other variable names, it's done in this manner:
let obj = {a: 1, b: 2};
let {a: x, b: y} = obj;
console.log(x, y); // prints 1 2
To get to the point, if you ever need to separate the first couple elements of an iterable from the trailing rest, it can be destructured alongside the spread operator in the following manner:
let arr = [1,2,3,4,5]
let [a,b,...rest] = arr
console.log(a,b,rest) // prints 1 2 [3,4,5]
This is cool because it allows us to simultaneously copy and object and exclude a specific property:
let obj = {a: 1, b: 2, c: 3}
let {b, ...objCopy} = obj
console.log(b, objCopy) // prints 2 {a: 2, c: 3}
The trailing parameter in this setting is also called a rest parameter. Note that this rest parameter needs to be placed in the last position for it to work.
Parameter Merging
I'm not entirely certain if this is the correct term for this technique, but a somewhat obscure application of the spread operator is merging multiple input parameters as a single input parameter:
function mergeParams(...params){
return params
}
console.log(mergeParams(1, 2, 3)) // prints [1, 2, 3]
Here the rest operator needs to be the last input parameter in the signature as well. This one's a bit out there, but you never know... it might come in handy someday.
Optional/Conditional Spreading
The most unusual application of the spread syntax, is one I came across in a post by Enmanuel Durán:
Essentially, we can give objects optional properties by making use of the spread operator:
const isDog = true;
const obj = {
key: 'value',
...(isDog && { woof: true })};
}
Here's also a reddit discussion about wether this is good or not, what do you think?
Closing Thoughts
In this post we've tackled the JS spread operator and several of it's use cases. If you want to learn another lesser used feature of Javascript you might also want to check this other post I've written on Javascript Generators:
That's it from me again - see you in the next post, until then cheers and happy coding ~ Gorilla Sun!