Like others before me, I found JavaScript’s prototype-based OOP support – while very powerful – quite cumbersome in some situations where I needed a certain structure or hierarchy of objects, situations in which one quickly comes to the conclusion that the best answer would be the concept of a Class.
However, while I am well aware that the web is filled with various implementations of classical (i.e. class-based) object orientation in JavaScript, I can not help the feeling that they are over-engineered and over-complicated without need, quite often attempting to provide more functionality than is actually necessary. In short, I don’t feel they’re “classical” at all.
This is why I started working on my own implementation of classical OOP in JavaScript, which is shown below. Currently this is only a proposal, although I already started using it in one of my projects. Anyway, let’s look at it first then I’ll try and explain how it works.
The Code
/** Class factory. */ function Class(members) { // proxy constructor var Proxy = function() { for (var item in members) { this[item] = members[item]; } }; // proxy inheritance Proxy.prototype = (members.base || Class).prototype; // class constructor var Build = members.init || function() {}; // class inheritance Build.prototype = new Proxy(); Build.prototype.base = Proxy.prototype; Build.prototype.constructor = Build; // ready return Build; }
That is all, and this little function can have a whole world of nifty consequences like deep inheritance or access to the super-class via this.base (and others). But here is what happens: the basic principle at work here is the Proxy object, which essentially, contains all the members of our new class (the ones that are passed to the Class factory function as the members
parameter).
So, instead of copying the given members straight into the newly created class, we put them into another object (i.e. the Proxy) and make that the prototype of our new class. This means that every class will point (via the prototype
property) to an internal Proxy object holding the actual class members. The trick is that this Proxy, in turn, points to the ancestor’s Proxy object (again via the .prototype
property) and so on.
The code (and further updates) may be found here.
The Concept
Here is a visual depiction of the system:
This trick enables one very important feature: classic class inheritance via the prototype
property. Class members are never duplicated and the only additional objects ever created are the Proxy objects (one per class). Here are some features:
The Simplicity
You can create a class very easily, using a constructor (i.e. init()
) and this class can have instance members, class members and/or static members:
var Person = Class({ // constructor init: function(fname, lname, age, job) { // class member this.first_name = fname; this.last_name = lname; // instance members this.age = age; this.job = job; }, // class members first_name: '', last_name: '', }); // static member Person.nationality = 'Romanian'; // create instance var man = new Person('George', 'Enescu', 33, 'Classical Composer');
The Deep Inheritance
Classes can have single, but deep, inheritance (i.e. inheritance chains) where each object has direct access to the super-class via the .base
property:
var A = Class({ hello: function() { console.log('Hello world!'); } }); var B = Class({ // inherit A base: A, aloha: function() { console.log('Aloha world!'); } }); var C = Class({ // inherit B base: B, aloha: function() { // override method from B console.log('Goodbye world!'); } }); var c = new C(); c.hello(); // 'Hello world!' c.aloha(); // 'Goodbye world!' c.base.aloha.call(c); // 'Aloha world!'
Note: the c.base.aloha()
method was invoked via call()
in order to apply it onto the c
object; invoking it as c.base.aloha()
would have applied it onto the c.base
object.
The Prototypal Concepts
The regular functionality of the prototype
property is kept intact, so that changes to A.prototype
will implicitly propagate to everything derived from class A
(all instances, all sub-classes, all instances of sub-classes etc.):
// extend class A A.prototype.doStuff = function() { console.log('Am doing some extra stuff!'); } // check any child c.doStuff(); // 'Am doing some extra stuff!'
[Update] This also means that you may affect the prototype of any class form an inheritance chain and it will do what you expect (i.e. propagate changes down the chain to all the children below that class).
Also the instanceof
operator behaves as expected:
console.log(c instanceof A); // true console.log(c instanceof B); // true console.log(c instanceof C); // true
The .prototype.constructor
property is also properly configured, so if you want, you can create new instances starting from an initial object, by extracting the object’s constructor:
// identical to C.prototype.constructor var Cons = Object.getPrototypeOf(c).constructor; // create new object var c2 = new Cons();
The Efficiency
Inheritance is provided without creating a new instance of the base class for every prototype. This is unnatural anyway since it calls the base constructor without arguments, which may have unwanted side-effects:
NewClass.prototype = new BaseClass(); // the weird way of prototyping
Also, class members are never copied between classes and no duplicates are ever created, thus saving memory and ensuring high access speed for the class members.
In addition, the entire code is only a few lines long (no, I’m not going to count), so it has a certain elegance to it.
The Conclusion
Maybe this technique is neither new, nor very full featured, but honestly, if I want mixins, extensible objects and other such exotic features, then why would I bother with old-school OOP classes anyway; the JavaScript prototypal approach is capable of all that, and more!
I’m not saying that this is *the* way of doing classic OOP in JavaScript, but I certainly feel it is more in-line with what I perceive to be the JavaScript mindset: keep it simple, use what you have at your disposal and don’t kill yourself trying to emulate other languages!
That being said I also think this code may be improved, so pitch in a comment or two if you like :D…
P.S.: For example, the Build() constructor may be easily modified to enforce the concept of abstract classes, effectively refusing to create the instance (i.e. via throw) if it detects that any of its members (inherited or not) is purely virtual (i.e. set to null
or undefined
). So a sub-class must either implement all the purely virtual members it inherits, or it will simply become abstract as well (I actually use this trick in production).
Comments are closed.