JS OOP Deep-Dive: Object.assign

In Launch School’s Object-Oriented Programming course Lesson 4 (JavaScript track) we learn about using mixins as a way of mimicking multiple inheritance, as JavaScript only allows for single inheritance. Put simply, mixins are used to share methods/behaviors amongst objects of different types.

[A full understanding of the above isn’t necessary for this article, but it would certainly help. See here for an explanation.]

We apply mixins via the Object.assign method. As per MDN, the Object.assign method “copies all enumerable own properties from one or more source objects to a target object,” like this:

prop ‘a’ from obj1 copied over to obj2

So far, so good. Like many goodies in JavaScript, though, the rabbit hole goes far deeper.

First of all, let’s ensure we understand the official definition of Object.assign. We covered ‘source’ and ‘target’ in the above code snippet. But what does ‘own enumerable’ mean?

‘Own’ is fairly straightforward — if you’ve gotten this far in the curriculum you know about prototypal delegation, which allows for an inheriting object to access the properties defined on the parent object. Those inherited properties are not considered the ‘own’ properties of the inheriting object, so they are not copied over to the target object via Object.assign. Example:

Next: ‘enumerable.’ Scary big word. What does that mean? Well, MDN says: “Enumerable properties are those properties whose internal enumerable flag is set to true, which is the default for properties created via simple assignment or via a property initializer.” Oh, come on! First rule of definitions: don’t use that same word in the definition…if I didn’t know what it meant before, I still don’t. A simplified albeit roundabout understanding: ‘enumerable’ means that the property will show up if you iterate over the object using the for..in loop or Object.keys. And we know from MDN this is default behavior for most properties. Ok, enough of that, that’s an adequate understanding of ‘enumerable’ for our Object.assign deep-dive. We’ll come back around to an example of enumerability at the end.

Now that we understand the definition of Object.assign’s functionality, let’s dig a little deeper.

Question: what happens if a source object has the same property names as the target object’s properties? Answer: the property on the target object will be overridden, like this:

prop ‘a’ on target object has been overridden by prop ‘a’ on source object

Also, we know from MDN’s definition that Object.assign can accept multiple source object arguments. Question: what happens when there are multiple same-named properties in those arguments? Answer: we go according to the last source object argument with that same property name, like this:

prop ‘a’ from last argument ‘obj3’ wins

When we say Object.assign copies the properties from one object to another, what sort of ‘copy’ is this? You may remember about shallow copies versus deep copies from course 101. Turns out, Object.assign creates shallow copies. Why should that matter to us?

Well, when comparing object creation patterns, we know that one disadvantage of the factory function model is that the methods defined on the factory function are copied over to each newly object created. And that’s bad for memory. We can demonstrate that we have distinct method objects on each new object by comparing their methods using the strict equality operator ===:

The false return value demonstrates that each object has their own (deep) distinct copy of the methods. Stated differently, each method points to a unique object in memory.

However, when we do the same comparison on methods copied over from mixins:

Here we get true! [Exclamation point because this chance discovery confused me and inspired me to write this article.] This indicates that the two methods actually point to the same object in memory, even though we can clearly see that each object has their own (shallow) copy of the mixin method using hasOwnProperty:

That’s good for memory, but a consequence of shallow copies is that any mutations to nested objects are reflected in other shallow copies of that mixin object.

A mutation can be made directly to the mixin:

Or a mutation can be made via the object’s own shallow copies:

Therefore, this behavior should be kept in mind when defining properties on mixin objects and sharing those properties via Object.assign.

You may think that mixin objects won’t have nested objects since we use mixins to share methods rather than nested objects— and you might be right. But, who’s to say how any given developer will define their mixin methods? Ultimately, leveraging shallow copies is beyond the scope of this article. Just beware of any nested objects defined on mixins, realizing that any mutations to those objects may lead to unintended bugs when copied viaObject.assign.

While we’re on the topic of mixins, another quirk of JavaScript is enumerable (yup, back to that word) properties in ES6 classes. Any methods defined directly on a class are actually non-enumerable, while any classes defined directly on the class prototype object or copied to the class prototype object via a mixin/Object.assign are enumerable.

Inspected via Object.keys(only enumerable) and Object.getOwnPropertyNames (also non-enumerable):

That concludes our foray into Object.assign and mixins.

More JavaScript discoveries await!