Spread Operator in JavaScript

Introduction

In this tutorial, we'll explore one of the powerful features of the ES6 specification of JavaScript - the Spread Operator. Although the syntax is simple, sometimes the implementation is confusing if you do not understand it properly. In this tutorial, we'll demystify those three dots ... of JavaScript that does amazing things with iterables.

Usages of the Spread Operator

There are different usages of the spread operator and each usage target to solve a different problem statement.

Expanding Arrays

We can use the spread operator on iterables like a String or an array and it'll put the contents of the iterable into individual elements.

For an example:

let greet = ['Hello', 'World'];
console.log(greet); // Without spread operator
console.log(...greet); // Using spread operator

If we run this code we'll see the following:

['Hello', 'World']
Hello World

You must have noticed that in the second case (with spread operator), the contents of the greet list were expanded and thrown out of the array.

Sometimes, we may feel the need to convert a String into a list of characters. We can use spread operator for this use-case:

let greetings = "hello";
let chars = [...greetings];
console.log(chars);

If we run this code, we'll be greeted with:

[ 'h', 'e', 'l', 'l', 'o' ]

These examples might not convince you of the usefulness this operator offers. In that name, let's take some real-world problems that can be solved with the spread operators.

Combining Arrays

Let us take advantage of the fact that we can now expand an array using the spread operator. Let's say we have subscriber lists from two different sources and we want to combine both these sources and make a single subscribers list:

let blog1Subscribers = ['[email protected]', '[email protected]'];
let blog2Subscribers = ['[email protected]', '[email protected]', '[email protected]'];
let subscribers = [...blog1Subscribers, ...blog2Subscribers];
console.log(subscribers);

If we run the above code, we'll get a single list of iterables. This was made possible as both ...blog1Subscribers and ...blog2Subscribers were spread out and the [] acted as the "receiver", which effectively combined the spread items into a single list of items.

Note: The spread operator needs the receiver to put the expanded value into. If you omit the receiver, it'll throw an error.

We can also use the spread operator inside the Array.push() method to push the contents of one array into another:

let arr1 = ['John', 'Sofia', 'Bob'];
let arr2 = ['Julia', 'Sean', 'Anthony'];
arr2.push(...arr2);
console.log(arr1);

If we run this code, we'll see the following output:

[ 'Julia', 'Sean', 'Anthony', 'John', 'Sofia', 'Bob' ]

Copying Arrays and Objects

In JavaScript every non-primitive entity is an Object, which means that arrays are also objects. You may know that objects are copied as a reference-type:

let arr1 = ['John', 'Sofia', 'Bob'];
let arr2 = arr1;
console.log(arr2);
arr1.push('Sally'); // Change arr1
console.log(arr2);
[ 'John', 'Sofia', 'Bob' ]
[ 'John', 'Sofia', 'Bob', 'Sally' ]

As expected, the values of the items in the array weren't copied, only the reference to them. We can solve this problem easily with the spread operator:

let arr1 = ['John', 'Sofia', 'Bob'];
let arr2 = [...arr1];
console.log(arr2);
arr1.push('Sally'); // Change arr1
console.log(arr2);

Running this code produces the following:

[ 'John', 'Sofia', 'Bob' ]
[ 'John', 'Sofia', 'Bob' ]

As we can see, the arr2 wasn't passed a reference like before, but rather it was populated with the values of arr1 as a whole new object. So even when arr1 changes, arr2 remains the same.

We can also use the spread operator to create a copy of an array and add new elements into it at the same time:

let arr1 = ['John', 'Sofia', 'Bob'];
let arr2 = [...arr1, 'Anthony', 'Sean'];
console.log(arr2);
['John', 'Sofia', 'Bob', 'Anthony', 'Sean']

Note: The spread operator works with all iterables, including Objects.

Previously this would have taken an extra line of code to add the new items to the new array.

Similarly we can copy objects by using the spread operator:

let o1 = { a: 1, b: 2 };
let o2 = { c: 3, d: 4, ...o1 };
console.log(o2);
{ c: 3, d: 4, a: 1, b: 2 }

As we can see we successfully copied the object o1 into o2.

This feature has many real-world use-cases. For example, let us say we stored user registration information into an object. We can make a shallow copy of that object and add some more information into the copied object:

let user = { name: 'John', email: '[email protected]' };
let _user = { ...user, ...getSession(user) };
console.log(_user);
{ name: 'John', email: '[email protected]', 'token': 'abc123', 'expiresAt': 1565630480671 }

We may also need to merge billing and shipping information into one:

const billing = { billingContact: '0987654321', billingAddress: 'street no 123, xyz city' };
const shipping = { shippingContact: '123456789', shippingAddress: 'street no 999, abc city' };
const custInfo = { ...billing, ...shipping };
console.log(custInfo);

If we run this code, we should be greeted with:

{
  billingContact: '0987654321',
  billingAddress: 'street no 123, xyz city',
  shippingContact: '123456789',
  shippingAddress: 'street no 999, abc city'
}

One question could be raised here. What if both objects have some of the same properties.

In case of clashing properties, the property of the last object wins. Let us see this in an example:

const o1 = { a: 1, b: 2 };
const o2 = { b: 3, c: 4, ...o1};
console.log(o2);

If you run this code, you should see the following:

{ b: 2, c: 4, a: 1 }

As we can see the properties of the second object o2 wins. However, if we put the spread operator first:

const o1 = { a: 1, b: 2 };
const o2 = { ...o1, b: 3, c: 4};
console.log(o2);
{ a: 1, b: 3, c: 4 }

We can see that the property from o1 wins, which makes sense since o2 is the last object.

One use case of this feature could be to make default assignments:

const userProvided = {
    name: 'Bil Smith',
    email: '[email protected]',
};
const defaultValues = {
    name: 'Unknown',
    address: 'Alien',
    phone: null,
    email: null
};
const userInfo = { ...defaultValues, ...userProvided };

Alternative to Calling Functions with apply()

Let us say a function takes an argument - a list of marks of top 5 students in a class. We also have a list coming from an external source. Surely, we can avoid passing individual items and instead pass the entire list by using the apply() method:

myFun(m1, m2, m3, m4, m5) {
    // Do something
}

let marks = [10, 23, 83, -1, 92];
myFun.apply(undefined, arr);

We can get rid of the confusing undefined argument and make the code cleaner by calling the function directly with the spread operator:

myFun(m1, m2, m3, m4, m5) {
    // Do something
}

let marks = [10, 23, 83, -1, 92];
myFun(...marks);

Using with Math Functions

JavaScript has a Math object which contains several methods to operate with a set of data, i.e. a list of data.

Let us say we want to get the maximum value from first three numbers of a list:

let mylist = [10, 23, 83, -1, 92, -33, 76, 29, 76, 100, 644, -633];
Math.max(mylist[0], mylist[1], mylist[2]);

What if we want to get the maximum of all numbers in a list? What if the list has n number of items? Surely we wont want mylist[0], mylist[1]... mylist[1000].

The spread operator provides a cleaner solution:

let mylist = [10, 23, 83, -1, 92, -33, 76, 29, 76, 100, 644, -633];
Math.max(...mylist);

Note: Given that the spread operator works with both Arrays and Objects, you may sometimes be tempted to mix and match them. Don't do that! For example, the following action will result in an error:

let user = {name:'John', age:28, email:'[email protected]'};
let items = [...user];
TypeError: user is not iterable

Spread Operator vs Rest Parameter

Both the Spread Operator and Rest Parameter share the same syntax i.e. the three magical dots .... But they behave exactly opposite to each other. As a beginner, sometimes this may be confusing. The bottom-line to understand the behavior is to understand the context in which it is being used.

As we learned, the spread operator expands the contents of an iterable. In contrast, the rest operator collects all the remaining elements into an array.

function doSum(...items) {
    let sum = 0;
    for (let item of items){
        sum += item;
    }
    return sum;
}

doSum(1);
doSum(1,2);
doSum(1, 2, 3, 4);

If we run the above code, we'll be greeted with the following:

1
3
6
10

As we can see, everytime the remaining elements were collected by the Rest Parameter.

We can also provide distinct variables for some of the elements and make the rest parameter collect the rest of the items. The only condition is that the rest parameter should always be the last parameter of the function:

function doSum(times, ...items) {
    let sum = 0;
    for (let item of items){
        sum += item*times;
    }
    return sum;
}

doSum(1, 1);
doSum(2, 1, 2);
doSum(3, 1, 2, 3);

If we run the above code, we'll see the following:

1
6
18

Conclusion

As we can see the spread operator ... is a really powerful feature of the ES6 specification of JavaScript. We can solve plenty of the real-world problems easily by using this operator. As we learned from the various examples discussed in this article, it lets us write less code and do more.

In this article, we covered the common usages of the Spread Operator. We also discussed the similar-looking, but different, Rest Parameter. Please be aware that there could be dozens of other use-cases depending on the problem.

Author image
Ireland Twitter Website
A Software Engineer who is passionate about writing programming articles. Founder of CosmoCode.io - Free Coding tutorials