Preface
To better understand what the proposal brings – we should get acquainted with rest and spread operators in the context of array destructuring and array literals.
Array Destructuring
As part of ES2015 (ES6), we introduced a new operator in array destructuring – the rest operator.
When destructuring an array – the rest operator combines the remaining elements of an array into a variable:
let [x, ...remaining] = [1, 2, 3, 4];
console.info(x); // 1
console.info(remaining); // [2, 3, 4]
We use destructuring assignment to pick the first element into x
and all the others into remaining
.
Note: The rest operator is symbolized by three dots (...
).
Array Literals
As part of ES2015 (ES6), we introduced a new operator in array literals – the spread operator.
When initializing an array – the spread operator copies the elements of an existing array into the new array:
let existingArray = [2, 3, 4];
let newArray = [1, ...existingArray];
console.info(newArray); // [1, 2, 3, 4]
We initialize a new array with the number 1
and all the elements of existingArray
.
Note: The spread operator is symbolized by three dots (...
).
The Proposal
Object Rest/Spread Properties is an approved proposal for ES2018 (ES9) specification that was created by Sebastian Markbåge. This proposal provides the ability to use rest and spread operators on top of objects.
Object Destructuring
Analogously to the array – when destructuring an object, the rest operator combines the remaining enumerable own properties of that object into a new object, without those which weren’t already picked off by the destructuring pattern:
let { x, ...remaining } = { x: 1, a: 2, b: 3, c: 4 };
console.info(x); // 1
console.info(remaining); // {a: 2, b: 3, c: 4}
We use destructuring assignment to pick the value of "x" property into x
and all the others enumerable own properties into an object which’s named remaining
.
On top of that, we can perform a shallow clone for an object:
let { ...shallowClone } = { x: 1, foo: () => {} };
console.info(shallowClone); // {x: 1, foo: ƒ}
All the enumerable own properties are copied to the new object, except for prototypes.
Sometimes we’ve nested objects. In that case, we could use the rest operator multiple times to pick off the values we actually want:
let nestedObject = {
x: [1, 2, 3],
y: { a: 4, b: 5, foo: () => {} }
};
let { x: [x1, ...remaining], ...allRemaining } = nestedObject;
console.info(x1); // 1
console.info(remaining); // [2, 3]
console.info(allRemaining); // {y: {a: 4, b: 5, foo: ƒ}}
This example is a bit tricky – because we use array destructuring assignment to pick the first element of “x” into x1
and then – inserting all other elements into remaining
. However, it’s easy to see that array destructuring is nested inside an object destructuring, so that, the second rest operator takes all the properties (which were already picked off) and inserts those into allRemaining
(which means the y
property in practice).
In addition to what’s being said, these cases are invalid – so be sure to avoid them:
let { ...x, y } = { a: 1, b: 2, c: 3 }; // Syntax error
let { x, ...y, ...z } = { a: 1, b: 2, c: 3 }; // Syntax error
let { ...x, y } = undefined; // Runtime error
Object Literals
Analogously to the array – when initializing an object, the spread operator copies the enumerable own properties of an existing object into the new object:
let existingObject = { x: 1, a: 2, b: 3, c: 4 };
let newObject = { ...existingObject, foo: () => { } };
console.info(newObject); // {x: 1, a: 2, b: 3, c: 4, foo: ƒ}
A new object is initialized with all the properties of existingObject
and followed by foo
function. Notice that the order is important, because it affects the insertion. In case we’d use the operator after foo
, we’d get that function as the first enumerable own property.
Moreover, performing a shallow clone is fairly simple:
let existingObject = { x: 1, foo: () => {} };
let newObject = { ...existingObject };
console.info(newObject); // {x: 1, foo: ƒ}
Notice that clone doesn’t include the prototypes as before.
Well, it seems like an easy way to clone (compared to Object.assign
), however, there are some differences:
- In case the cloned object has setters,
Object.assign
would trigger them, as opposed to the spread operator. - Using
Object.assign
, we can prevent from creating own properties via inherited read-only properties, while the spread operator is unable to do it.
Let’s take advantage of this operator to merge between objects:
let sampleObject = { x: 1, y: 2 };
let sampleObject2 = { z: 3 };
let sampleObject3 = { a: [ 'b', 'c' ] };
let newObject = { ...sampleObject, ...sampleObject2, ...sampleObject3 };
console.info(newObject); // {x: 1, y: 2, z: 3, a: ['b', 'c'])}
For the record, it’s equivalent to the following line:
let newObject = Object.assign({ }, sampleObject, sampleObject2, sampleObject3);
Here’s how to override properties from an existing object:
let sampleObject = { x: 1, y: 2 };
let newObject = { ...sampleObject, x: 3 };
console.info(newObject); // {x: 3, y: 2}
Let’s see how we define default values for properties:
let sampleObject = { x: 'a', y: 'b' };
let newObject = { z: 'c', ...sampleObject, };
console.info(newObject); // {z: 'c', x: 'a', y: 'b'}
In case our sampleObject
has a z
property – it’ll be overridden as we’ve seen before.
Summary
We explored today the rest and spread operators in terms of objects, and we learned how to use these in object destructuring and object literals.
Here are some useful references:
- The official proposal
- Examples for using the rest opeator
- Examples for using the spread opeator
- "Rest/Spread Properties" by Axel Rauschmayer
Here are the examples which were shown in the article: