Object spread vs. Object.assign
By Delicious Insights • Published on May 18, 2020

This post is also available in French.

Let’s get started with our fifteenth post in our daily series “19 nuggets of vanilla JS,” this time to clear things up on the roles and respective specificities of the object spread syntax that became official with ES2018, as opposed to the Object.assign(…) API formalized in ES2015. As you’ll see, these are not quite the same thing.

The series of 19

Check out surrounding posts from the series:

  1. const is the new var
  2. Using named captures
  3. Object spread vs. Object.assign (this post)
  4. Converting an object to Map and vice-versa
  5. The for-of loop: should there remain only one…
  6. …and beyond! (fear not, all 19 are scheduled already)…

From extend(…) to assign(…)

In JavaScript, we’ve always needed to copy properties (and their values) from one object to another. Whether we’re trying to build an options hash, create a descriptor or something else, this is a common scenario.

Considering that it is rather tedious to do manually (for…in loop, hasOwnProperty(…) safeguard, and more), we quickly saw the emergence of utility functions from third-party libraries, and ultimately in the language’s standard library.

The story of Object.assign(…) goes way back already:

  1. In 2004, Prototype.js popularizes Object.extend(…).
  2. Soon enough, jQuery 1.0 features jQuery.extend(…) (usually called as $.extend(…)), that generalizes the signature to allow for not just one source object, but as many sources as we’d like, which becomes the accepted signature for this kind of operation; jQuery will then extend (1.1.4) its semantics to offer deep merging in addition to the original, shallow merge.
  3. Underscore and then Lodash expectedly feature _.extend(…), again with any number of sources (Lodash also offers the much older _.assign(…), which is closer to what ES2015 will standardize).
  4. In 2015, the standard library for the language grows with ES2015 and features Object.assign(…), that copies all own enumerable properties from sources to the destination (with the good manners of ignoring undefined source arguments, making our calling code easier).

Here are a few quick call examples:

function run(options) {
  // Aggregates in a new `options` object (that starts out
  // empty, `{}`) the options that will be used in the end,
  // by first copying default options, than overriding these
  // with the actually-passed ones.
  options = Object.assign({}, DEFAULT_OPTIONS, options)
  // …
}

const desc = {
  url: "/api/v1/register",
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    Accept: "application/json",
  },
}
// Extends / replaces request headers with those that may
// have been passed, ensuring a few mandatory headers
// will make it through (e.g. CORS-related ones).
Object.assign(desc.headers, customHeaders, ensuredHeaders)

ES2018 and the object spread

When the React team designed the JSX syntax, it proposed an extension that quickly grew popular: the spread of props, that lets us use an object’s properties as a sort of dynamic bag of props:

function CoolBeans(props) {
  return (
    <button {...props} className="cool-btn">
      Cool beans!
    </button>
  )
}

ES2015 featured a spread on iterables (e.g. Array and String), but not on any plain object: we had to wait for ES2018 to get Rest/Spread Properties. The syntax can be used exclusively inside an object literal, thus in the creation of a new object. Just like the spread on iterables, we can use multiple object spreads in a literal. The order only matters when several spread properties share the same name: the last relevant spread wins.

const bruce = { first: "John", last: "McLane", city: "NYC" }
const eddie = { first: "Axel", city: "Beverly Hills", birthday: "1959-08-25" }

const mashup = { ...bruce, ...eddie, birthday: "1977-11-04" }
// => { first: 'Axel', last: 'McLane', city: 'Beverly Hills',
//      birthday: '1977-11-04' }

With object spreads, it becomes easy to quickly derive an object from another, which makes it a particularly popular syntax when writing immutable reducers, as with Redux:

function reduceOverall(state, { type, payload }) {
  switch (type) {
    case TALLY_DAY:
      return {
        ...state,
        history: tally(state),
        progresses: {},
        today: dateToISO(),
      }
  }
}

A subtle difference

You might think that the two bits of code below are equivalent:

let result = {
  /* … */
}
// v1…
Object.assign(result, {
  persisted: true,
  signature: hmac(result),
})
// or v2?
result = { ...result, persisted: true, signature: hmac(result) }

In this specific example, the two results will indeed end up the same. The target of Object.assign(…) is a new empty object of type Object, with no existing writer accessors.

However, when code using Object.assign(…) writes to a more advanced object, perhaps one with writer accessors, things start to differ: the second version creates a new object of type Object, it doesn’t write in the original result. This means that not only might you change the underlying type of the object (result might have been an instance of a custom class of yours), but you’re killing any guarantees or built-in behaviors its writer accessors ensured.

class Person {
  constructor(first, last) {
    this.first = first
    this.last = last
  }

  get fullName() {
    return `${this.first} ${this.last}`
  }

  set fullName(value) {
    ;[this.first, this.last] = value.split(/\s+/)
  }
}

let result = new Person("Amélie", "Poulain")
Object.assign(result, { fullName: "Nino Quincampoix" })
result // => Person { first: 'Nino', last: 'Quincampoix' }
result.fullName // => 'Nino Quincampoix'
result instanceof Person // => true

result = { ...result, fullName: "Raymond Dufayel" }
result
// => { first: 'Nino', last: 'Quincampoix', fullName: 'Raymon Dufayel' }
result instanceof Person // => false

So there you have it. It’s not quite the same thing, but that’s rarely an issue in practice. Just remember that an object spread always returns a new object, of type Object, and will therefore blissfully ignore the type and writer accessors of the original object(s).

Incidentally, this means that when you must alter the original object, perhaps for object identity purposes, you can only go with assign(…).

Depending on the specifics of your situation and need, you’ll either go with assign(…) or an object spread. Choose wisely.

Where can I get that?!

As for Object.assign(…), it’s been natively supported since Chrome 45, Firefox 34, Opera 32, Edge 12, Safari 9 and Node 4.

Object spreads showed up more recently but a while ago still: in Chrome 60, Firefox 55, Opera 47, Edge 79, Safari 11.1 and Node 8.3.

As always, for older platforms core-js and polyfill.io provide the former, and Babel can transpile the latter.

Check out our video course: JavaScript: this is it! 🖥

Get in-depth understanding of how this works in JavaScript, from core ground rules to API overrides to arrow functions, binding and much, much more!