This article about Mixins in ES6 has been one of the best technical reads in a good while. It's pretty amazing how straight forward it's to create mixins thanks to ES6 class expressions, and how much better this approach is (you can use "super" for example) than the old one of augmenting existing objects. Read the article, it's an eye opener.
Bearing in mind that ES6 classes are mainly sugar to create prototype chains, one question arises then, how could we do this with "traditional" javascript?
First let's think a bit of how this new technique works. Both at the "sugar level - class level" and the prototype level, Javascript's has a single inheritance model. Each object in the prototype chain points only to another object. What we do here when defining a class made up by adding mixins to another existing class, is to put these mixins one after another in the new prototype chain. So each time we mixin the same block of behaviour to a different class we have to create a new mixing object holding that behavior to add it into this specific prototype chain. That's the key, we are defining the mixins by means of factory functions that create new classes (subclasses), classes that then we mix into the inheritance chain . I'd never thought of mixins exacty in this way. The definition given in the article is excellent:
A mixin is an abstract subclass; i.e. a subclass definition that may be applied to different superclasses to create a related family of modified classes.
The first point is just to check that this is really working this way. Let's create a new class based on some mixins and print the prototype chain. I'm using named class expressions for the mixins, so that I get something meaningful when printing the prototype chain with my simple traversal function.
function printPrototypeChain(obj){ let protoNames = []; let curProto = Object.getPrototypeOf(obj); while (curProto){ protoNames.push(curProto.constructor.name); curProto = Object.getPrototypeOf(curProto); } console.log("prototype chain:\n" + protoNames.join(".")); } function testNewStyleMixins(){ console.log("- testNewStyleMixins"); var calculatorMixin = Base => class __calculatorMixinGeneratedSubclass extends Base { calc() { } }; var randomizerMixin = Base => class __randomizerMixinGeneratedSubclass extends Base { randomize() { } }; //A class that uses these mix-ins can then be written like this: class Foo { } class Bar extends calculatorMixin(randomizerMixin(Foo)) { } let bar1 = new Bar(); printPrototypeChain(bar1); //Bar.__calculatorMixinGeneratedSubclass.__randomizerMixinGeneratedSubclass.Foo.Object }
To set up something like this in "traditional" javascript, we'll use factories of functions (constructor functions that we turn into "classes" by attacching functions to its prototype). The previous example translates to this:
function testOldStyleMixins(){ console.log("- testOldStyleMixins"); function Foo(){ } function CalculatorMixin(fn){ let _calculatorMixin = function _calculatorMixinGeneratedFunction(){ }; _calculatorMixin.prototype = new fn(); _calculatorMixin.prototype.constructor = _calculatorMixin; _calculatorMixin.prototype.calc = function(){}; return _calculatorMixin; } function RandomizerMixin(fn){ let _randomizerMixin = function __randomizerMixinGeneratedFunction(){ }; _randomizerMixin.prototype = new fn(); _randomizerMixin.prototype.constructor = _randomizerMixin; _randomizerMixin.prototype.randomize = function(){}; return _randomizerMixin; } function Bar(){ }; //WATCH OUT!!! if I do "new CalculatorMixin(RandomizerMixin(Foo));" I get a completely different thing!!! Bar.prototype = new (CalculatorMixin(RandomizerMixin(Foo))); Bar.prototype.constructor = Bar; let bar1 = new Bar(); printPrototypeChain(bar1); //Bar._calculatorMixinGeneratedFunction.__randomizerMixinGeneratedFunction.Foo.Object }
It's important to note that with ES6 mixins the access to properties and method in the base classes via super works perfectly fine. This is something to bear in mind, while ES6 classes are for the most part syntactic sugar, in order to make them work ES6 added new internal structures, like target.new and [[HomeObject]], as explained in the fantastic Exploring ES6
I got a bit confused when in the article he gives examples of inheriting and composing mixins. I'll add some notes to his examples for further reference.
Applying multiple Mixins. This is just the normal use. I'm creating a new class that extends the class resulting from mixing into MyBaseClass Mixin1 and Mixin2
class MyClass extends Mixin2(Mixin1(MyBaseClass)) { //specific methods here //... }
Mixin Inheritance. Here I create a new class factory function (Mixin3). It applies other 2 existing mixins to the provided subclass and adds new methods to it. For that it's indeed creating an additional class in the prototype chain: ClassA -> Mixin3GeneratedSubclass -> Mixin2GeneratedSubClass -> Mixin1GeneratedSubclass -> superclass
let Mixin3 = (superclass) => class Mixin3GeneratedSubclass extends Mixin2(Mixin1(superclass)) { //specific methods here //... }
Mixin Composition. Here I create a new class factory function by combining 2 existing mixins, but it's not adding new methods of its own. That's the difference with the previous case, it does not need to add one more class. When used we'll get: ClassA -> Mixin2GeneratedSubClass -> Mixin1GeneratedSubclass -> superclass.
let Mixin3 = (superclass) => Mixin2(Mixin1(superclass));
I have put a sample in this gist
There's a follow up article that moves into more advanced features, like cacing and making instaceof work with them. Real food for brain.
No comments:
Post a Comment