A faster and easier way to define Javascript Prototypal Inheritance: classes and mixins
| Node: | Nuget: |
|---|---|
| npm install fast-class | Install-Package Javascript-FastClass |
- Among
popular libraries define + usage- 3 classes and 3 methods * 500 instances each Fastest libraries define + usage- 3 classes and 3 methods * 500 instances eachFastest libraries define only- 3 classes and 3 methods * 1 instances each- Unit tests - so you can see it just works
varVehicle=Function.define(function(name)){//define the "base class"this.name=name;//constructor initializer},{//optionally specify prototype members i.e. to be added on Vechicle.prototypedraw: function(){console.log("Drawing a "+this.name+" vehicle.");}});//optionally specify extra mixins i.e. , Wheels);varCar=Vechicle.inheritWith(function(base,baseCtor)){//define the "derrived class"return{//optionally specify prototype membersconstructor: function(name,color){//optionally specify a custom construcorbaseCtor.call(this,name);//ensure you are calling the baseCtorthis.color=color;},draw: function(){//redefine the draw function on Car.prototypeconsole.log("Drawing a "+this.color+" car.");base.draw.call(this);//optionally call the base class' draw method}}},Engine);//optionally specify extra mixins whose prototype members will be copied to Car.prototype//Define the Engine mixinfunctionEngine(){//this constructor will be automatically called when creating any class using it //(including derrived classes). i.e. new Car() in this example this.powerSource='petrol';//these properties will be added to every Car instancethis.cmc=1400;// i.e. var c= new Car(); c.cmc = 1400this.horsePower=140;}.define({//optioanlly add custom methods on Engine.prototype mixin//these will be copied to Car.prototype in our example, baecause it references the Engine mixinisElectric: function(){returnthis.pwerSource==='electric';}})vartoyota=newCar("toyota prius","red");//creating a new Cartoyota.powerSouce;//petroltoyota.powerSource='electric';//changing the powerSource property toyota.isElectric();//returns truetoyota.draw();//Drawing a red car. //Drawing a tyota prius vechicle.Native javascript inheritance is a pin in the ass. Even if you understand it perfectly it still requires some hideous repetitive code.
There are a lot of libraries which aim to help you with such that, but the main question is:
What is the fastest vs most convenient (also known as: with the most sugar) to create Prototypal Inheritance with?
You might want this library
- when you can't use a language that
compilesinto javascript code. - because it is fast
- you love
writing less - doing more!
e.g. Google Closure, TypeScript, Coffee Script etc.
FastClass is a very tiny library (~1KB minified and gzipped) that helps you quickly derive your classes so to speak.
It comes in two flavours:
Function.prototype.fastClass(creator)- sets theBase.prototypeto thecreatorfunction and then callsnew creator(this.prototype, this)
function(base,baseCtor){this.somePrototypeMethod1= ...;this.somePrototypeMethod2= ...;}}Function.prototype.inheritWith(creator)- recommended returns aplain objectcontaining the members of the new prototype, including the constructor itself
It makes usage of proto on all new browsers (which makes it blazing fast) except Internet Explorer <= 10 and maybe other ancient browsers where it fallbacks to for (var key in obj) statement.
Note __proto__ will become standard in ECMAScript 6
function(base,baseCtor){return{somePrototypeMethod1: ...,somePrototypeMethod2: ...}}whereas baseCtor is the function we want to inherit and base is it's prototype (baseCtor.prototype that is).
varFigure=function(name){this.name=name;Function.initMixins(this);// since this is a top function, we need to call initMixins to make sure they will be initialized.}Figure.prototype.draw=function(){console.log("figure "+name);}You can define the first class' constructor function (same as above but with sugar syntax) as following:
varA=function(name){this.name=name;Function.initMixins(this);// since this is a top function, we need to call initMixins to make sure they will be initialized.}.define({draw: function(){console.log("figure "+this.name);}}/*, add any mixins here */)The define function copies all the members of the returned object to the function.prorortpe(A.prototype) and returns it (A)
This way we don't need to call Function.initMixins(this) as it will be automatically called for us
varA=Function.define(function(name){this.name=name;},{draw: function(){//method added on function(name) {}.prototypeconsole.log("figure "+this.name);}}Alternatively you can pass an object with the constructor function specified (similar syntax to .inheritWith)
varA=Function.define({constructor: function(name){//the constructor itself can be even missing. If so we will add one for you!this.name=name;},draw: function(){//method added on constructor.prototypeconsole.log("figure "+this.name);}}All of the above methods are doing the same thing. You decide which one better suits you.
A classical example to use inheritance when you have a base class called Figure and a derived class called Square.
To define the derrived class Square:
varSquare=Figure.fastClass(function(base,baseCtor){this.constructor=function(name,length){this.length=length;baseCtor.call(this,name);//calls the bacse ctor}this.draw=function(){//redefines the draw functionconsole.log("square with length "+this.length);base.draw.call(this);//calls the base class' draw function}})To define the derrived class Square:
varSquare=Figure.inheritWith(function(base,baseCtor){return{constructor: function(name,length){this.length=length;baseCtor.call(this,name);//calls the bacse ctor},draw: function(){//redefines the draw functionconsole.log("square with length "+this.length);base.draw.call(this);//calls the base class' draw function}}})As you can see in both cases the definition is pretty simple and very similar.
However the .inheritWith flavour comes with about 5-15% performance boost depending on the actual browser and number of members.
That is because we are simply setting __proto__ with the BaseClass.prototype for those browsers who support it (all nowdays browsers except IE<=10)
In both cases we the constructor is the function that is returned as the derrived class' constructor.
The constructor is optional and we should only add it when we have some custom code as both functions will add it for us otherswise.
Whichever flavour you choose the code usage is the same. Firstly you need to instantiate the Constructor with the new operator:
varfigure=newFigure("generic");figure.draw();//figure genericvarsquare=newSquare("10cm square",10);figure.draw();//square with length 10//figure 10cm squareMixins are some grouped functionalities that you can add to a class without inheritance
You can learn more about mixins vs inheritance on this post.
We can define mixins as
- an
objectcontaining functions - a
functionwhich will be executed for every instance of the class that is using it- The
prototypeof the function will be automatically copied to theclass.prototypethat are using it
- The
// Animal base classfunctionAnimal(){// Privatefunctionprivate1(){}functionprivate2(){}// Privileged - on instancethis.privileged1=function(){}this.privileged2=function(){}Function.initMixins(this);}.define({// Public - on prototypemethod1: function(){console.log("Animal::method1");}});Alternatively the above can be defined as:
varAnimal=Function.define(function(){// Private functionprivate1(){}functionprivate2(){}// Privileged - on instancethis.privileged1=function(){}this.privileged2=function(){}},{method1: function(){console.log("Animal::method1");}};});Or even simpler as an object providing the constructor:
varAnimal=Function.define({constructor: function(){// the constructor is optional// Private functionprivate1(){}functionprivate2(){}// Privileged - on instancethis.privileged1=function(){}this.privileged2=function(){}},method1: function(){console.log("Animal::method1");}});The function Animal is the function given as first parameter.
It acts as the constructor, which is invoked when an instance is created:
varanimal=newAnimal();// Create a new Animal instanceWe can typically define a move action which is a set of methods grouped in our Move mixin
functionMove(){//define the constructor of the mixin. This will be automatically called for every instance in the constructor of the class that is using this mixinthis.position={x: 0,y: 0};}.define({//define mixin's prototype. These will be copied to the prototype of the classes that will use this `mixin`moveTo: function(x,y){this.position.x=x;this.position.y=y;},resetPosition: function(){this.position.x=0;this.position.y=0;},logPosition: function(){console.log("Position: ",this.position);}});[[constructor]].inheritWith( function(base, baseCtor) { return {...} }, mixins ) - that function should return methods for the derrived prototype
// Extend the Animal class with inheritWith flavorvarDog=Animal.inheritWith(function(base,baseCtor){//derrived class containing some private method(s)functionsomeOtherPrivateMethod(){}return{// Override base class `method1`method1: function(){someOtherPrivateMethod.call(this);base.method1.call(this);//calling a methd from the base class: Animal.prtotype.method1console.log('Dog::method1');},scare: function(){console.log('Dog::I scare you');},//some new public method(s)method2: function(){}}},Move);//specify any extra mixins to be added to the Dog's prototypeCreate an instance of Dog:
varhusky=newDog();husky.method1();// Animal::method1// Dog::method1husky.scare();// Dog::I scare you'/// testing the mixin:husky.logPosition();// Call the method of the mixin. // Position: {x: 0, y: 0}husky.moveTo(10,20);husky.logPosition();// Position: {x: 10, y: 20}husky.resetPosition();husky.logPosition();// Position: {x: 0, y: 0}[[constructor]].fastClass( function(base, baseCtor) { this.m = function {...} ... } ) - that function populates the derrived prototype
Every class definition has access to the parent's prototype via the first argument passed into the function. The second argument is the base Class itself (its constructor i.e. Animal in our case):
// Same as above but Extend the Animal class using fastClass flavorvarDog=Animal.fastClass(function(base,baseCtor){//private functionsfunctionsomeOtherPrivateMethod(){};//since we don't set this.constructor, automatically will be added one for us which will call the baseCtor with all the provided arguments//this is importat so we can set the new prototype into it// Override base class `method1`this.method1=function(){someOtherPrivateMethod.call(this);// Call the parent functionbase.method1.call(this);console.log('Dog::method1');};this.scare=function(){console.log('Dog::I scare you');};//some more public functionsthis.method2=function(){};},Move);You can sometime extend a constructor or a function with some static methods
In order to do so you need to call .defineStatic({...})
varF=function(){}.defineStatic({loadData: function(){...}}).typeofF.loadData// returns functionF.loadData()//call the static functionThis can be easily mixed with Function.define
varF=Function.define({constructor: function(){}).defineStatic({//make sure you are adding it here and not on the constructor function itself. //that is because Function.define will automatically declare a function in order //to ensure Function.initMixins gets automatically called//add any static members hereloadData: function(){...}};typeofF.loadData// returns functionF.loadData()//call the static functionThe Function.initMixins will always check for duplicate members that are already defined in the object i.e. the class defines a member whith the same name, or another mixin defines it.
If such collision is found an exception will be thorwn when using the debug version of the library. Using the .min file there is no error and the member will be overriden.
However in order to prevent this exception you can define a function as abstract with .defineStatic(obj):
varAnimal=Function.define({method1: function(){thrownewException("You need to implement method1 on your class's or mixin's prototype")}.defineStatic({abstract: true});//make this function abstract so it can be overriden by mixins});varf=Function.abstract();f();//will trigger a failed assert with message "Not implemented"varg=Function.abstract("Custom not implemented message");g();//will trigger a failed assert with message "Custom not implemented message"f.abstract===true;//truevarh=Function.abstract(function(){/* will be called before the assert "Not implemented" will be thrown */});h();//will call the given function and then will trigger a failed assert with message "Not implemented"varj=Function.abstract("Custom not implemented message",function(){/* will be called before the assert "Not implemented" will be thrown */});j();//will call the given function and then will trigger a failed assert with message "Custom not implemented message"Beside GitHub, you can download it as a Nuget package in Visual Studio fromhere
Install-PackageJavascript-FastClassThere is also a nodejs version
npminstallfast-classDo you have a better & faster way? Share it! We would love to seeing creativity in action!