DEV Community

Cover image for The tale of three dots in Javascript
Gábor Soós
Gábor Soós

Posted on • Updated on • Originally published at sonicoder.com

The tale of three dots in Javascript

One upon a time, there was a significant upgrade to the Javascript language called ES6/ES2015. It introduced many different new features. One of them was the three consecutive dots that we can write in front of any compatible container (objects, arrays, strings, sets, maps). These tiny little dots enable us to write a more elegant and concise code. I'll explain how the three dots work and show the most common use-cases.

The three consecutive dots have two meanings: the spread operator and the rest operator.

Spread operator

The spread operator allows an iterable to spread or expand individually inside a receiver. The iterable and the receiver can be anything that can be looped over like arrays, objects, sets, maps. You can put parts of a container individually into another container.

const newArray = ['first', ...anotherArray];
Enter fullscreen mode Exit fullscreen mode

Rest parameters

The rest parameter syntax allows us to represent an indefinite number of arguments as an array. Named parameters can be in front of rest parameters.

const func = (first, second, ...rest) => {};
Enter fullscreen mode Exit fullscreen mode

Use-cases

Definitions can be useful, but it is hard to understand the concept just from them. I think everyday use-cases can bring the missing understanding of definitions.

Copying an array

When we have to mutate an array but don't want to touch the original one (others might use it), we have to copy it.

const fruits = ['apple', 'orange', 'banana'];
const fruitsCopied = [...fruits]; // ['apple', 'orange', 'banana']

console.log(fruits === fruitsCopied); // false

// old way
fruits.map(fruit => fruit);
Enter fullscreen mode Exit fullscreen mode

It is selecting each element inside the array and placing each of those elements in a new array structure. We can achieve the copying of the array with the map operator and making an identity mapping.

Unique array

We want to sort out duplicate elements from an array. What is the simplest solution?

The Set object only stores unique elements and can be populated with an array. It is also iterable so we can spread it back to a new array, and what we receive is an array with unique values.

const fruits = ['apple', 'orange', 'banana', 'banana'];
const uniqueFruits = [...new Set(fruits)]; // ['apple', 'orange', 'banana']

// old way
fruits.filter((fruit, index, arr) => arr.indexOf(fruit) === index);
Enter fullscreen mode Exit fullscreen mode

Concatenate arrays

We can concatenate two separate arrays with the concat method, but why not use the spread operator again?

const fruits = ['apple', 'orange', 'banana'];
const vegetables = ['carrot'];
const fruitsAndVegetables = [...fruits, ...vegetables]; // ['apple', 'orange', 'banana', 'carrot']
const fruitsAndVegetables = ['carrot', ...fruits]; // ['carrot', 'apple', 'orange', 'banana']

// old way
const fruitsAndVegetables = fruits.concat(vegetables);
fruits.unshift('carrot');
Enter fullscreen mode Exit fullscreen mode

Pass arguments as arrays

When passing arguments is where the spread operator starts making our code more readable. Before ES6, we had to apply the function to the arguments. Now we can just spread the parameters to the function, which results in much cleaner code.

const mixer = (x, y, z) => console.log(x, y, z);
const fruits = ['apple', 'orange', 'banana'];

mixer(...fruits); // 'apple', 'orange', 'banana'

// old way
mixer.apply(null, fruits);
Enter fullscreen mode Exit fullscreen mode

Slicing an array

Slicing is more straightforward with the slice method, but if we want it, the spread operator can be used for this use-case also. We have to name the remaining elements one-by-one, so it is not a great way to slice from the middle of a big array.

const fruits = ['apple', 'orange', 'banana'];
const [apple, ...remainingFruits] = fruits; // ['orange', 'banana']

// old way 
const remainingFruits = fruits.slice(1);
Enter fullscreen mode Exit fullscreen mode

Convert arguments to an array

Arguments in Javascript are array-like objects. You can access it with indices, but you can't call array methods on it like map, filter. Arguments are an iterable object, so what can we do with it? Put three dots in front of them and access them as an array!

const mixer = (...args) => console.log(args);
mixer('apple'); // ['apple']
Enter fullscreen mode Exit fullscreen mode

Convert NodeList to an array

Arguments are like a NodeList returned from a querySelectorAll function. They also behave a bit like an array but don't have the appropriate methods.

[...document.querySelectorAll('div')];

// old way
Array.prototype.slice.call(document.querySelectorAll('div'));
Enter fullscreen mode Exit fullscreen mode

Copying an object

Finally, we get to object manipulations. Copying works the same way as with arrays. Earlier it was doable with Object.assign and an empty object literal.

const todo = { name: 'Clean the dishes' };
const todoCopied = { ...todo }; // { name: 'Clean the dishes' }
console.log(todo === todoCopied); // false

// old way
Object.assign({}, todo);
Enter fullscreen mode Exit fullscreen mode

Merge objects

The only difference in merging is that properties with the same key get overwritten. The rightmost property has the highest precedence.

const todo = { name: 'Clean the dishes' };
const state = { completed: false };
const nextTodo = { name: 'Ironing' };
const merged = { ...todo, ...state, ...nextTodo }; // { name: 'Ironing', completed: false }

// old way
Object.assign({}, todo, state, nextTodo);
Enter fullscreen mode Exit fullscreen mode

It is important to note, that merging creates copies only on the first level in the hierarchy. Deeper levels in the hierarchy will be the same reference.

Splitting a string into characters

One last with strings. You can split a string into characters with the spread operator. Of course, it is the same if you would call the split method with an empty string.

const country = 'USA';
console.log([...country]); // ['U', 'S', 'A']

// old way
country.split('');
Enter fullscreen mode Exit fullscreen mode

And that's it

We looked at many different use-cases for the three dots in Javascript. As you can see ES6 not only made it more efficient to write code but also introduced some fun ways to solve long-existing problems. Now all the major browsers support the new syntax; all the above examples can be tried in browser console while reading this article. Either way, you start using the spread operator and the rest parameters. It is an excellent addition to the language that you should be aware of.

Top comments (19)

Collapse
 
wormss profile image
WORMSS • Edited

You have a bug in your code.

// old way
const fruitsAndVegetables = fruits.concat(vegetables);
const fruitsAndVegetables = fruits.push('carrot');

The second line will set fruitsAndVegetables to the count of the new length of the array. Not the reference to the array itself

Collapse
 
sonicoder profile image
Gábor Soós

Thanks for noting!

Collapse
 
wormss profile image
WORMSS

Hmmm, now you are mutating fruit array.
Are you sure you were not trying to do something like

const fruitsAndVegetables = fruits.concat(vegetables);
const fruitsAndVegetables = fruits.slice();
fruitsAndVegetables.unshift('carrot');

I don't believe there is a neat little 1 liner to do the equivalent without wrapping 'carrot' in a temp array.

Thread Thread
 
sonicoder profile image
Gábor Soós

I feel the same way...the new syntax is much more compact.

Collapse
 
adam_cyclones profile image
Adam Crockett 🌀

I still use the "old way" about as much as the new way ... because I'm old? Actually, no, I think I am used to manipulating things with methods not operators. But JavaScript is changing, pipeline, bind operators for example.

Fun things to know: spread does exist in other languages but in most cases it doesn't do as much as it does in JavaScript.

Collapse
 
sonicoder profile image
Gábor Soós

It is an addition, syntactic sugar, old things still work. It has its pros and cons.

Collapse
 
adam_cyclones profile image
Adam Crockett 🌀 • Edited

Everything is syntax, valid or no it's still parsed and lexed to build an AST, so "part of the syntax" seems broad. If it isn't an operator perhaps it's more like a comma or semi token? But those describe white space, spread does a heck of a lot more, I'm not sure what it is now... Sorry to nitpick 😅

Collapse
 
jamesernator profile image
James Browning

If we don't need the exact arguments before the rest, we don't have to name them. This example also works:

This isn't correct, parameters always have to be named, none of the popular engines support this.

Collapse
 
sonicoder profile image
Gábor Soós

Thanks for noting

Collapse
 
danielo515 profile image
Daniel Rodríguez Rivero

I was about to say the same. What he wrote is just wrong syntax. I may work if it is compiled to regular function, but in a native implementation it will not work.

Collapse
 
sonicoder profile image
Gábor Soós

It is just old, not wrong, still runs in Chrome. Bad syntax would be non backward compatible breaking change.

Thread Thread
 
danielo515 profile image
Daniel Rodríguez Rivero

It is not just old, is wrong. You can of course declare such function because the permissive nature of javascript, but if you try to run it you are going to get an error because arguments is an undefined variable. You can see it yourself in the (hopefully) attached screenshot.
But you don't have to trust me, is on the spec: developer.mozilla.org/en-US/docs/W...

Thread Thread
 
sonicoder profile image
Gábor Soós

I had a misunderstanding here, thanks for linking the documentation

Thread Thread
 
danielo515 profile image
Daniel Rodríguez Rivero

No problem. JS is a tricky language (sometimes 😄).

 
adam_cyclones profile image
Adam Crockett 🌀

As soon as I wake up tomorrow, I'm gonna check this out in some console, I ask because 'assign' doesn't copy getters and setters, having used that for years and years, it came as a bit of a shock.

Thread Thread
 
worsnupd profile image
Daniel Worsnup

I, too, was curious about this, so I decided to run some code in the console! It looks like both spread and assign will copy "normal" getters, but they will not copy getters that are explicitly marked as non-enumerable using Object.defineProperty or Reflect.defineProperty:

const obj1 = {
  get test() { return 'test' }
}

const obj2 = {}
Reflect.defineProperty(obj2, 'test', {
  enumerable: false,
  get() { return 'test' }
})

console.log(Object.keys(obj1)) // ['test']
console.log(Object.keys(obj2)) // []

const obj1Spread = { ...obj1 }
const obj1Assign = Object.assign({}, obj1)

const obj2Spread = { ...obj2 }
const obj2Assign = Object.assign({}, obj2)

console.log(Object.keys(obj1Spread)) // ['test']
console.log(Object.keys(obj1Assign)) // ['test']
console.log(Object.keys(obj2Spread)) // []
console.log(Object.keys(obj2Assign)) // []

However, when an object is an instance of a class that defines a getter, the getter is on the object's prototype and so it doesn't show up during enumeration and therefore doesn't get copied with object spread/assign:

class MyClass {
  get test() { return 'test' }
}

const obj = new MyClass()
const objSpread = { ...obj }

console.log(Object.keys(obj), Object.keys(objSpread)) // [], []

I would assume this behavior to be consistent with setters.

Collapse
 
sonicoder profile image
Gábor Soós

Noted, updated the example.

Collapse
 
adam_cyclones profile image
Adam Crockett 🌀

Do setters and getters and discriptors get coppied?

Collapse
 
sonicoder profile image
Gábor Soós

Added it as a warning, thanks.