What is the difference between Object.assign and spread Operator?

Asked

Viewed 941 times

9

What is the detailed difference of using Object.assign and the Operator spread (...) for spreading properties of objects?

For example, when editing an object for new values, it is the same output:

const cursoNaDB = {
  abreviacao: "ES5",
  nome: "EcmaScript5",
  ano: "2009",
  categoria: "INICIANTE"
}

const cursoParaAlterarNaDB = {
  abreviacao: "ES6",
  nome: "EcmaScript6",
  ano: "2015",
  categoria: "AVANÇADO"
}

console.log(Object.assign(cursoNaDB, cursoParaAlterarNaDB));

console.log({ ...cursoNaDB, ...cursoParaAlterarNaDB });

1 answer

10


There are some subtle differences:

Difference in the definition of properties

The first of these is that Object.assign uses internal operation [[Set]] to define properties. Scattering notation (Operator spread) will simply set the properties. So, Object.assign will call the setters, but the Operator spread no. See an example:

const target = {
  set foo(val) {
    console.log('Chamou o set:', val);
  }
};
const other = { foo: 123 };

// Object.assign chama o setter:
console.log(
  Object.assign(target, other)
);

console.log('---');

// Spread operador não chama o setter:
console.log(
  { ...target, ...other }
);

Note, in the example above, that as the Operator spread didn’t even call the Setter, the property foo was defined as 123 on the resulting object. However, when using Object.assign, the value we set for foo is not persisted in the resulting object, since the Setter does nothing with it (apart from printing it on the console).

Thus, it can be stated that the Object.assign works in a similar way to allocation operator (=), which also performs the internal operation [[Set]] (what invokes the setters, if any). The Operator spread works more similar to Object.defineProperty, that does not perform setters:

const obj = {
  set foo(val) {
    console.log('Chamou o setter: ', val);
  }
};

// Object.assign se assemelha a isto:
obj.foo = 123;
console.log(obj); // { foo: undefined }

console.log('---');

// Spread operator se assemelha a isto:
Object.defineProperty(obj, 'foo', {
  value: 123
});
console.log(obj); // { foo: 123 }

Possible difference in the constructor of the resulting object

Another difference is that the prototype and constructor of the resulting object may be different from Object when using Object.assign. When using the Operator spread, the constructor of the resulting object is, always, Object:

class DifferentThanObject {
  a = 1;
  b = 2;
}

const instance = new DifferentThanObject();
const other = { c: 3, d: 4 };

const obj1 = Object.assign(instance, other);
console.log(obj1.constructor.name); // DifferentThanObject

const obj2 = { ...instance, ...other };
console.log(obj2.constructor.name); // Object

To understand, we need to be aware of the signature of Object.assign:

Object.assign(target, ...sources)

The difference of builders happens, then, since Object.assign "copies" all enumerable properties of each object source (passed from the second argument forward) to the destination (target, the first argument). Thus, the constructor of the resulting object will always be the same as the object target.

As we saw in the example above, how instance (past as target to the Object.assign) is an instance of DifferentThanObject, the output will also be an instance of DifferentThanObject.

To language specification for Object.assign makes this exceptionally clear (refer to the link to the complete algorithm):

Perform ? Set(to, nextKey, propValue, true).

Being to the object that was passed as target, nextKey the key to the property to be defined and propValue the value of this.

This shows that a new object is not even created. For each property of objects source, the operation will be executed [[Set]] in the target. That is, the same object that enters as target is the one who leaves, only with properties set too.

However, whenever we use the Operator spread to make this "transfer" of properties, the constructor of the resulting object will be Object. This is because we always create a new literal object (which is, by definition, built by Object):

const literal = {};
console.log(literal.constructor.name); // Object

The difference can be better understood if we try to imitate the behavior of Object.assign and of Operator spread explicitly:

class DifferentThanObject {
  a = 1;
  b = 2;
}

const instance = new DifferentThanObject();
const other = { c: 3, d: 4 };

// ------- "Imitando" `Object.assign`:
// Object.assign(instance, other);

const resulting1 = instance;
resulting1.c = other.c;
resulting1.d = other.d;

console.log(resulting1);

// ------- "Imitando" spread operator:
// { ...instance, ...other };

const resulting2 = {};
Object.defineProperty(resulting2, 'a', { value: instance.a, enumerable: true });
Object.defineProperty(resulting2, 'b', { value: instance.b, enumerable: true });
Object.defineProperty(resulting2, 'c', { value: other.c, enumerable: true });
Object.defineProperty(resulting2, 'd', { value: other.d, enumerable: true });

console.log(resulting2);

Note that when using Operator spread, we are "obliged" to create a new literal object (with the notation {}) that the properties may be spread over it. The Object.assign will simply set the properties to any value passed to your first argument.

You can pass an empty object like target in the Object.assign if you want to ensure that only your own and enumerable properties will be transferred, no longer "transfer" the constructor and prototype properties of the object target:

class DifferentThanObject {
  // Métodos são anexados ao protótipo:
  method() {
    // noop
  }
  
  // Propriedades da instância:
  a = 1;
}

const instance = new DifferentThanObject();
const other = { b: 2 };

const resulting1 = { ...instance, ...other };
console.log(resulting1.constructor.name); // Object
console.log(typeof resulting1.method); // undefined //=> Métodos do protótipo NÃO são "transferidos".
console.log(resulting1); // { a: 1, b: 2 }

console.log('---');

const resulting2 = Object.assign(instance, other);
console.log(resulting2.constructor.name); // DifferentThanObject
console.log(typeof resulting2.method); // function //=> Métodos do protótipo são "transferidos".
console.log(resulting2); // { a: 1, b: 2 }

console.log('---');

//        Objeto literal "vazio" ↓↓
const resulting3 = Object.assign({}, instance, other);
console.log(resulting3.constructor.name); // Object
console.log(typeof resulting3.method); // undefined //=> Métodos do protótipo NÃO são "transferidos".
console.log(resulting3); // { a: 1, b: 2 }

This happens because, in this last example, target is a literal object (whose constructor is Object) and, as we have seen, the Object.assign results in the very target, only with the properties of other objects (sources) arrows.

Is it worth reading What is the difference between creating an object from the literal form or from a constructor function? to better understand.

Completion

In your case, how are you not dealing with setters and the objects have the same constructor (Object, since both were created literally), there is no real difference.

The only caveat is that Object.assign is a little more supported than the Operator spread for scattering properties of objects. That’s because Object.assign was introduced in Ecmascript 2015 (ES6), while the Operator spread only gained the ability to spread object properties in Ecmascript 2018.

  • 1

    Cool, that’s why the Object.assign works well with Vue.

Browser other questions tagged

You are not signed in. Login or sign up in order to post.