The Object Rest/Spread Proposal reached stage 4 in 2018, which means it will be included in a future iteration of the ECMAScript spec. It's also been included in Node.js LTS since Node.js 8, so you can safely start using it today.

$ node -v
v8.9.4
$ node
> const obj = { foo: 1, bar: 1 };
undefined
> { ...obj, baz: 1 };
{ foo: 1, bar: 1, baz: 1 }

The Object spread operator {...obj} is similar to Object.assign(), so which one should you use? Turns out the answer is a bit more nuanced than you might expect.

Quick Overview of Object Spread

The fundamental idea of the object spread operator is to create a new plain object using the own properties of an existing object. So {...obj} creates a new object with the same properties and values as obj. For plain old JavaScript objects, you're essentially creating a copy of obj.

const obj = { foo: 'bar' };
const clone = { ...obj }; // `{ foo: 'bar' }`
obj.foo = 'baz';
clone.foo; // 'bar'

Like Object.assign(), the object spread operator does not copy inherited properties or class information. It does copy ES6 symbols.

class BaseClass {
  foo() { return 1; }
}

class MyClass extends BaseClass {
  bar() { return 2; }
}

const obj = new MyClass();
obj.baz = function() { return 3; };
obj[Symbol.for('test')] = 4;

// Does _not_ copy any properties from `MyClass` or `BaseClass`
const clone = { ...obj };

console.log(clone); // { baz: [Function], [Symbol(test)]: 4 }
console.log(clone.constructor.name); // Object
console.log(clone instanceof MyClass); // false

You can also mix in other properties with the object spread operator. Order matters: the object spread operator will overwrite properties that are defined before it, but not after.

const obj = { a: 'a', b: 'b', c: 'c' };
{ a: 1, b: null, c: void 0, ...obj }; // { a: 'a', b: 'b', c: 'c' }
{ a: 1, b: null, ...obj, c: void 0 }; // { a: 'a', b: 'b', c: undefined }
{ a: 1, ...obj, b: null, c: void 0 }; // { a: 'a', b: null, c: undefined }
{ ...obj, a: 1, b: null, c: void 0 }; // { a: 1, b: null, c: undefined }

Differences Versus Object.assign()

The Object.assign() function is essentially interchangeable with the object spread operator for the above examples. In fact, the object spread spec explicitly states that { ...obj } is equivalent to Object.assign({}, obj).

const obj = { a: 'a', b: 'b', c: 'c' };
Object.assign({ a: 1, b: null, c: void 0 }, obj); // { a: 'a', b: 'b', c: 'c' }
Object.assign({ a: 1, b: null }, obj, { c: void 0 }); // { a: 'a', b: 'b', c: undefined }
Object.assign({ a: 1 }, obj, { b: null, c: void 0 }); // { a: 'a', b: null, c: undefined }
Object.assign({}, obj, { a: 1, b: null, c: void 0 }); // { a: 1, b: null, c: undefined }

So why would you use one or the other? One key difference is that the object spread operator always gives you a POJO back. The Object.assign() function modifies its first parameter in place:

class MyClass {
  set val(v) {
    console.log('Setter called', v);
    return v;
  }
}
const obj = new MyClass();

Object.assign(obj, { val: 42 }); // Prints "Setter called 42"

In other words, Object.assign() modifies an object in place, and so it can trigger ES6 setters. If you prefer using immutable techniques, the object spread operator is a clear winner. With Object.assign(), you would have to ensure you always pass an empty object {} as the first argument.

Another related difference is that spread defines new properties, whereas Object.assign() sets them. For example, Object.assign() calls setters that are defined on Object.prototype, whereas the spread operator does not.

Object.defineProperty(Object.prototype, 'myProp', {
  set: () => console.log('Setter called');
});

const obj = { myProp: 42 };

Object.assign({}, obj); // Prints "Setter called"

const newObj = { ..obj }; // Does **not** print "Setter called"

This is a fairly minor difference, because it is generally bad practice to define a custom setter on Object.prototype. But, you should note that Object.assign() calls setters on the target object.

const obj = {};

Object.defineProperty(obj, 'myProp', {
  set: () => console.log('Setter called');
});

Object.assign(obj, { myProp: 42 }); // Prints "Setter called"

What about performance? Here's a couple simple benchmarks. It looks like object spread is faster if you pass an empty object as the first parameter to Object.assign(), but otherwise they're interchangeable.

Here's a benchmark using Object.assign() with in-place assignment:

const Benchmark = require('benchmark');

const suite = new Benchmark.Suite;

const obj = { foo: 1, bar: 2 };

suite.
  add('Object spread', function() {
    ({ baz: 3, ...obj });
  }).
  add('Object.assign()', function() {
    Object.assign({ baz: 3 }, obj);
  }).
  on('cycle', function(event) {
    console.log(String(event.target));
  }).
  on('complete', function() {
    console.log('Fastest is ' + this.filter('fastest').map('name'));
  }).
  run({ 'async': true });

In this case, the two are similar:

Object spread x 3,170,111 ops/sec +-1.50% (90 runs sampled)
Object.assign() x 3,290,165 ops/sec +-1.86% (88 runs sampled)
Fastest is Object.assign()

However, once you throw in an empty object parameter to Object.assign(), the object spread operator is consistently faster:

suite.
  add('Object spread', function() {
    ({ baz: 3, ...obj });
  }).
  add('Object.assign()', function() {
    Object.assign({}, obj, { baz: 3 });
  })

Here's the output:

Object spread x 3,065,831 ops/sec +-2.12% (85 runs sampled)
Object.assign() x 2,461,926 ops/sec +-1.52% (88 runs sampled)
Fastest is Object spread

ESLint Configuration

By default, ESLint disallows the object rest/spread operator at the parser level. You need to set parserOptions.ecmaVersion option to at least 9 in .eslintrc.yml, otherwise you'll get a parsing error.

parserOptions:
  # Otherwise object spread causes 'Parsing error: Unexpected token ..'
  ecmaVersion: 9

ESLint added a new rule prefer-object-spread that allows you to enforce using object spread instead of Object.assign(). To enable this rule, use:

parserOptions:
  ecmaVersion: 9
rules:
  prefer-object-spread: error

Now ESLint will report an error if you use Object.assign() instead of object spread.

Use an object spread instead of `Object.assign` eg: `{ ...foo }`  prefer-object-spread

Moving On

The object rest/spread operators are both syntactically neat and offer performance benefits over Object.assign(). If you're running Node.js 8 or higher, try these new operators out and make your code more concise.

Found a typo or error? Open up a pull request! This post is available as markdown on Github
comments powered by Disqus