JavaScript – Extend vs Mixin

javascriptmixinsmultiple-inheritance

After having read Eric Elliott's Fluent JavaScript article, I was and still am toughtful about the way to play with instance prototypes.

On one side, you have the extending inheritance…

var B = function() {} ;
B.prototype = new A() ;

Or the new way of doing it…

B.prototype = Object.create(A.prototype) ;

And on the other side, you have the mixin, creating a mix of multiple prototypes that will be inherited to instances.

var B = function() {} ;
// Basic mixin from A
for (var m in A.prototype) B.prototype[m] = A.prototype[m] ;

  • In his article, Eric Elliott encourages us to avoid extending types but to compose (mixin) sort of components objects/prototypes instead (see also Stampit on github). I totally agree with that.

  • However, it seems to break one major feature (and philosophy) in JavaScript : Modularity. What I mean is the fact that altering prototypes will be automatically available for all instances by references.

Here is a basic example of the prototype inheritance :

var A = function() {} ;
A.prototype.fn = function() { return 1 ; } ;

var a = new A() ;
a.fn() ; // 1

A.prototype.fn = function() { return 2 ; } ;
a.fn() ; // 2

And now with an extended prototype :

var B = function() {} ;
B.prototype = Object.create(A.prototype) ;

var b = new B() ;
b.fn() ; // 2, inherited from A.prototype

Okay this is working well, however we can't inherit from multiple prototypes ; plus classical inheritance is generally avoided.

The other solution would be mixin then :

var D = function() {} ;
// Let's imagine a mixin function mixing D with some prototypes...
mixin(D, A.prototype, C.prototype, R.prototype) ;

var d = new D() ;
d.fn() ; // 2, inherited from A.prototype ? Let's see...

A.prototype.fn = function() { return 3 ; } ;
d.fn() ; // 2, what a shame...

Indeed, mixin only copy all functions from a prototype to another. This means there is no more references between those types, and thus modularity looks broken here (also tested with Stampit).


I'm now thinking of a tweaky solution to keep references to original prototype objects and not only the functions inside.

What about cascading inheritances ?

  • Not like D <- C <- B <- A, as it will alter B and C and looks restrictive.

  • But like D <- (((p <- A) <- B) <- C), where p is a private prototype reserved for D only. Neither B nor C will be altered, only p will be a straight cascade inheritance. D will have all methods from the listed prototypes, and if we want to inherit from another one, just restart the process from zero, like D <- ((((p <- A) <- B) <- C) <- R).

The only drawback I see would be around performances. Each time we add a new inheritance, we'll have to restart the whole process : Indeed, after inheriting, we add new functions to the prototype (or override some), and this must be always at the end of the process, thus after R inheritance :

  • Before : D <- ((((p <- A) <- B) <- C) <- {}) where {} is the set of new/overriding functions.

  • After : D <- (((((p <- A) <- B) <- C) <- R) <- {}), and not ... <- {}) <- R).

Would it be interesting to have a little library for that ? I would be up for doing this, allowing multiple prototype inheritances.


So… what do you think about this all ?

Best Answer

With Stampit, there is intentionally no link between prototype mixins and the original source, and that's by design, because altering the prototype of any instance could then alter the original prototype.

In your post, you seem to think that's a bad thing, but in practice, mutating prototypes after object instantiation has a myriad of wide-ranging side effects that can significantly impact both the robustness of the app (it's much harder to understand what changes will impact which objects. i.e., you think you're fixing a bug in your sprite animations, and suddenly your particle rendering engine breaks, because they share a common prototype), and performance (altering prototypes causes JS engines to throw away all their cached object property optimizations, including optimizations that impact the perf of every piece of code that uses any descendent object).

In other words, what you're trying to do is widely considered an anti-pattern in the JavaScript community.

Another interesting point: The new Stamp specification allows you to pass an instance into the stamp. If you do so, the stamp will not alter the original prototype, and the original prototype will actually appear in the prototype chain, meaning that what you're suggesting will be possible in the next version of Stampit -- but my warning that altering prototypes after instantiation is an anti-pattern still stands.

Related Topic