ES2018: Rest/Spread Properties

[2016-10-02] dev, javascript, esnext, es2018
(Ad, please don’t block)

Update 2018-01-25: This proposal has reached stage 4 and will be part of ECMAScript 2018.


The ECMAScript proposal “Rest/Spread Properties” by Sebastian Markbåge enables:

  • The rest operator (...) in object destructuring. At the moment, this operator only works for Array destructuring and in parameter definitions.

  • The spread operator (...) in object literals. At the moment, this operator only works in Array literals and in function and method calls.

The rest operator (...) in object destructuring  

Inside object destructuring patterns, the rest operator (...) copies all enumerable own properties of the destructuring source into its operand, except those that were already mentioned in the object literal.

const obj = {foo: 1, bar: 2, baz: 3};
const {foo, ...rest} = obj;
    // Same as:
    // const foo = 1;
    // const rest = {bar: 2, baz: 3};

If you are using object destructuring to handle named parameters, the rest operator enables you to collect all remaining parameters:

function func({param1, param2, ...rest}) { // rest operator
    console.log('All parameters: ',
        {param1, param2, ...rest}); // spread operator
    return param1 + param2;
}

Syntactic restrictions  

Per top level of each object literal, you can use the rest operator at most once and it must appear at the end:

const {...rest, foo} = obj; // SyntaxError
const {foo, ...rest1, ...rest2} = obj; // SyntaxError

You can, however, use the rest operator several times if you nest it:

const obj = {
    foo: {
        a: 1,
        b: 2,
        c: 3,
    },
    bar: 4,
    baz: 5,
};
const {foo: {a, ...rest1}, ...rest2} = obj;
// Same as:
// const a = 1;
// const rest1 = {b: 2, c: 3};
// const rest2 = {bar: 4, baz: 5};

The spread operator (...) in object literals  

Inside object literals, the spread operator (...) inserts all enumerable own properties of its operand into the object created via the literal:

> const obj = {foo: 1, bar: 2, baz: 3};
> {...obj, qux: 4}
{ foo: 1, bar: 2, baz: 3, qux: 4 }

Note that order matters even if property keys don’t clash, because objects record insertion order:

> {qux: 4, ...obj}
{ qux: 4, foo: 1, bar: 2, baz: 3 }

If keys clash, order determines which entry “wins”:

> const obj = {foo: 1, bar: 2, baz: 3};
> {...obj, foo: true}
{ foo: true, bar: 2, baz: 3 }
> {foo: true, ...obj}
{ foo: 1, bar: 2, baz: 3 }

Common use cases for the object spread operator  

In this section, we’ll look at things that you can use the spread operator for. I’ll also show how to do these things via Object.assign(), which is very similar to the spread operator (we’ll compare them in more detail later).

Cloning objects  

Cloning the enumerable own properties of an object obj:

const clone1 = {...obj};
const clone2 = Object.assign({}, obj);

The prototypes of the clones are always Object.prototype, which is the default for objects created via object literals:

> Object.getPrototypeOf(clone1) === Object.prototype
true
> Object.getPrototypeOf(clone2) === Object.prototype
true
> Object.getPrototypeOf({}) === Object.prototype
true

Cloning an object obj, including its prototype:

const clone1 = {__proto__: Object.getPrototypeOf(obj), ...obj};
const clone2 = Object.assign(
    Object.create(Object.getPrototypeOf(obj)), obj);

Note that __proto__ inside object literals is only a mandatory feature in web browsers, not in JavaScript engines in general.

True clones of objects  

Sometimes you need to faithfully copy all own properties of an object obj and their attributes (writable, enumerable, ...), including getters and setters. Then Object.assign() and the spread operator don’t work. You need to use property descriptors:

const clone1 = Object.defineProperties({},
    Object.getOwnPropertyDescriptors(obj));

If you additionally want to preserve the prototype of obj, you can use Object.create():

const clone2 = Object.create(
    Object.getPrototypeOf(obj),
    Object.getOwnPropertyDescriptors(obj));

Object.getOwnPropertyDescriptors() is explained in “Exploring ES2016 and ES2017”.

Pitfall: cloning is always shallow  

Keep in mind that with all the ways of cloning that we have looked at, you only get shallow copies: If one of the original property values is an object, the clone will refer to the same object, it will not be (recursively, deeply) cloned itself:

const original = { prop: {} };
const clone = Object.assign({}, original);

console.log(original.prop === clone.prop); // true
original.prop.foo = 'abc';
console.log(clone.prop.foo); // abc

Various other use cases  

Merging two objects obj1 and obj2:

const merged = {...obj1, ...obj2};
const merged = Object.assign({}, obj1, obj2);

Filling in defaults for user data:

const DEFAULTS = {foo: 'a', bar: 'b'};
const userData = {foo: 1};

const data = {...DEFAULTS, ...userData};
const data = Object.assign({}, DEFAULTS, userData);
    // {foo: 1, bar: 'b'}

Non-destructively updating property foo:

const obj = {foo: 'a', bar: 'b'};
const obj2 = {...obj, foo: 1};
const obj2 = Object.assign({}, obj, {foo: 1});
    // {foo: 1, bar: 'b'}

Specifying the default values for properties foo and bar inline:

const userData = {foo: 1};
const data = {foo: 'a', bar: 'b', ...userData};
const data = Object.assign({}, {foo:'a', bar:'b'}, userData);
    // {foo: 1, bar: 'b'}

Spreading objects versus Object.assign()  

The spread operator and Object.assign() are very similar. The main difference is that spreading defines new properties, while Object.assign() sets them. What exactly that means is explained later.

The two ways of using Object.assign()  

There are two ways of using Object.assign():

First, destructively (an existing object is changed):

Object.assign(target, source1, source2);

Here, target is modified; source1 and source2 are copied into it.

Second, non-destructively (no existing object is changed):

const result = Object.assign({}, source1, source2);

Here, a new object is created via an empty object literal and source1 and source2 are copied into it. At the end, this new object is returned and assigned to result.

The spread operator is very similar to the second way of using Object.assign(). Next, we’ll look at where the two are similar and where they differ.

Both spread and Object.assign() read values via a “get” operation  

Both operations use normal “get” operations to read property values from the source, before writing them to the target. As a result, getters are turned into normal data properties during this process.

Let’s look at an example:

const original = {
    get foo() {
        return 123;
    }
};

original has the getter foo (its property descriptor has the properties get and set):

> Object.getOwnPropertyDescriptor(original, 'foo')
{ get: [Function: foo],
  set: undefined,
  enumerable: true,
  configurable: true }

But it its clones clone1 and clone2, foo is a normal data property (its property descriptor has the properties value and writable):

> const clone1 = {...original};
> Object.getOwnPropertyDescriptor(clone1, 'foo')
{ value: 123,
  writable: true,
  enumerable: true,
  configurable: true }

> const clone2 = Object.assign({}, original);
> Object.getOwnPropertyDescriptor(clone2, 'foo')
{ value: 123,
  writable: true,
  enumerable: true,
  configurable: true }

Spread defines properties, Object.assign() sets them  

The spread operator defines new properties in the target, Object.assign() uses a normal “set” operation to create them. That has two consequences.

Targets with setters  

First, Object.assign() triggers setters, spread doesn’t:

Object.defineProperty(Object.prototype, 'foo', {
    set(value) {
        console.log('SET', value);
    },
});
const obj = {foo: 123};

The previous piece of code installs a setter foo that is inherited by all normal objects.

If we clone obj via Object.assign(), the inherited setter is triggered:

> Object.assign({}, obj)
SET 123
{}

With spread, it isn’t:

> { ...obj }
{ foo: 123 }

Object.assign() also triggers own setters during copying, it does not overwrite them.

Targets with read-only properties  

Second, you can stop Object.assign() from creating own properties via inherited read-only properties, but not the spread operator:

Object.defineProperty(Object.prototype, 'bar', {
    writable: false,
    value: 'abc',
});

The previous piece of code installs the read-only property bar that is inherited by all normal objects.

As a consequence, you can’t use assignment to create the own property bar, anymore (you only get an exception in strict mode; in sloppy mode, setting fails silently):

> const tmp = {};
> tmp.bar = 123;
TypeError: Cannot assign to read only property 'bar'

In the following code, we successfully create the property bar via an object literal. This works, because object literals don’t set properties, they define them:

const obj = {bar: 123};

However, Object.assign() uses assignment for creating properties, which is why we can’t clone obj:

> Object.assign({}, obj)
TypeError: Cannot assign to read only property 'bar'

Cloning via the spread operator works:

> { ...obj }
{ bar: 123 }

Both spread and Object.assign() only consider own enumerable properties  

Both operations ignore all inherited properties and all non-enumerable own properties.

The following object obj inherits one (enumerable!) property from proto and has two own properties:

const proto = {
    inheritedEnumerable: 1,
};
const obj = Object.create(proto, {
    ownEnumerable: {
        value: 2,
        enumerable: true,
    },
    ownNonEnumerable: {
        value: 3,
        enumerable: false,
    },
});

If you clone obj, the result only has the property ownEnumerable. The properties inheritedEnumerable and ownNonEnumerable are not copied:

> {...obj}
{ ownEnumerable: 2 }
> Object.assign({}, obj)
{ ownEnumerable: 2 }