Following on an older post of mine, I decided to write a new article about an(other) implementation of OOP Classes in JavaScript. This is something that brings a lot of value from very few lines of code and I want to share it with everyone and see how it can be improved.
First let’s see the code (you should check here for the latest version) and then I’ll try and explain the relevant benefits/features but jump straight to the goodies if you like, knock yourself out! 🙂
/**
* Class.js: A class factory.
*/
function Class(members) {
// setup proxy
var Proxy = function() {};
Proxy.prototype = (members.base || Class).prototype;
// setup constructor
members.init = members.init || function() {
if (Proxy.prototype.hasOwnProperty("init")) {
Proxy.prototype.init.apply(this, arguments);
}
};
var Shell = members.init;
// setup inheritance
Shell.prototype = new Proxy();
Shell.prototype.base = Proxy.prototype;
// setup identity
Shell.prototype.constructor = Shell;
// setup augmentation
Shell.grow = function(items) {
for (var item in items) {
if (!Shell.prototype.hasOwnProperty(item)) {
Shell.prototype[item] = items[item];
}
}
return Shell;
};
// attach members and return the new class
return Shell.grow(members);
}
Observations
Each Class
points to (via .prototype
) a corresponding Proxy
object that holds the class’s member fields. In turn, this Proxy
points to (via .prototype
) the Proxy
of the inherited class (i.e. the super-Class
) and so on. So, each Class
points to its own Proxy
which points to the ancestor Proxy
and so on, until the top level ancestor is reached.
This method is inherently very uncomplicated (it uses the fundamental facilities provided by the JavaScript language) and it is also very fast, causing no CPU or memory overhead on object creation and usage (only one extra object per class is created, the Proxy
object).
Inheritance (.base)
One of the most important benefits of the Class
factory presented here is support for real single-inheritance chains also known as deep single-inheritance (many implementations only provide superficial inheritance, i.e. only the immediate ancestor is inherited).
So when a member is not found in an Class’s instance (i.e. an object), the interpreter will look for it in the Class’s ancestor, then in the ancestor’s ancestor and so on until it either finds it (on some level) or fails.
This also enables the ability to override inherited members in sub-classes while still being able to access the original members using .call()
or .apply()
on them (i.e. obj.base.func.call(this, ...)
). This is needed because if we call obj.base.func(...)
directly, the this
object inside func()
will point to obj.base
and not obj
as we would expect (see examples below).
Constructor (.init)
All classes are created with a default constructor (if the init
constructor function is not given when defining a class) which does nothing other than to call the ancestor constructor (if applicable).
Augmentation (.grow)
In addition, each Class
provides the static method .grow()
that can be used to augment that class at any later time with new member definitions. The new members are immediately available (through inheritance) to all sub-classes (including already existing instances of those classes).
Obviously the same effect is achieved by adding members to the .prototype
of a class, but .grow()
is more simple to use and consistent with class creation (if fact, even the Class()
factory uses .grow()
in the first place).
Identity (instanceof)
The .prototype.constructor
field is properly set so that all classes can be used along with their instances with the instanceof
operator (see examples below).
Documentation (by example)
Following is a unit-test snippet (complete with comments) that showcases the features and characteristics of this implementation in concrete examples:
var assert = require('assert');
/* Class.js code goes here. */
/**
* Deep inheritance; members are searched up the inheritance chain
* until the top ancestor is reached.
* Here the .aloha() method is called on object c which is of type
* C, a sub-class of B which is itself a sub-class of A.
*/
var A = Class({
aloha: function() {
return "Class A says Aloha!";
}
});
var B = Class({
base: A
});
var C = Class({
base: B
});
var c = new C();
assert(c.aloha() === "Class A says Aloha!");
/**
* Class augmentation; the new members are immediately available to
* all sub-classes (including existing instances of those classes).
* Here the .bye() member is added to the B class and is immediately
* available in object c which is of type C, a sub-class of B.
*/
B.grow({
bye: function() {
return "Class B says Bye!";
}
});
assert(c.bye() === "Class B says Bye!");
/**
* Member overriding; inherited members can be overriden in sub-classes.
* Here C.aloha() overrides A.aloha() and takes precedence over it when
* being called on object c which is of type C.
*/
C.grow({
aloha: function() {
return "Class C says Cheers!";
}
});
assert(c.aloha() === "Class C says Cheers!");
/**
* Ancestor members calls; all the inherited members which have been
* overridden in sub-classes can still be called by explicit calls
* using .call() or .apply().
* Here, method C.bye() overrides B.bye() but the original method is
* still invoked both internally (from C.bye() via this.base) as well
* as externally (via c.base or B.prototype.bye.call()).
*/
C.grow({
bye: function() {
return "Class C says Cheerio and " + this.base.bye.call(this);
}
});
assert(c.bye() === "Class C says Cheerio and Class B says Bye!");
assert(c.base.bye.call(c) === "Class B says Bye!");
assert(B.prototype.bye.call(c) === "Class B says Bye!");
/**
* Constructors; all classes can have constructor methods, named init.
* Here, the D class constructor takes one parameter and increases
* member x with it.
*/
var D = Class({
x: 0,
init: function(_delta) {
this.x += _delta;
}
});
var d = new D(2012);
assert(d.x === 2012);
/**
* Implicit constructors; classes created without an init method are
* given an implicit constructor, which calls the super constructor.
* Here, class E has an implicit constructor which invokes D's init
* method (internally) on itself, therefore increasing x properly.
*/
var E = Class({
base: D,
x: 1000
});
var e = new E(234);
assert(e.x === 1234);
/**
* Class identity; all classes can be properly correlated with their
* instances using the instanceof operator.
* Here, several object instances are identifies against some classes
* and super-classes.
*/
assert(c instanceof C);
assert(c instanceof B);
assert(c instanceof A);
assert(!(c instanceof D));
assert(e instanceof E);
assert(e instanceof D);
/**
* Reflection; the class of an object instance can be obtained so that
* it may be used to create new object instances of the same type.
* Here, the class type is extracted from the object instance e in two
* ways (via the classic Object.getPrototypeOf() and via the .init()
* constructor method) and then used to instantiate two other objects.
*/
var Type1 = e.init;
var Type2 = Object.getPrototypeOf(e).constructor;
var e1 = new Type1(1);
var e2 = new Type2(2);
assert(e1.x === 1001);
assert(e2.x === 1002);
That’s it for now! 😀
All (constructive) comments are welcome so… help me better this!
Also, let me know if this is useful to you somewhere!