practical-inheritance
v1.1.2
Published
A true prototypal inheritance pattern that effectively replaces the classical constructor pattern
Downloads
11
Readme
The practical inheritance pattern has been designed to standarize the inheritance pattern used within iai modules. It has been published independently, attached to the following paper:
An alternative to the constructor pattern
Principles
Simplicity & expressiveness
The object declarations should not be verbose and must be expresive. Vanilla javascript is enough expresive but sometimes is verbose. That said, sometimes vanilla javascript produces the most simple and most descriptive code, so encourage to not use an utility function when it does not provide benefits for readability of code.
Targets
#1: Forget the concept of classes.
Everything is a instance in javascript, so talking about classes, even in quotes, is a mistake. There is no excuse. This target leads to the second:
#2: Completely eliminate the use of the
new
keyword.
The use of the new
keyword produces code that looks like a class instantiation, confusing readers. In addition, the use of new
is not combinable with the ECMAScript Function.prototype
functionalities, specially call
and apply
, and breaks possible chained calls.
~~constructors~~ builders
Research about creational design patterns and cross out definitions that refer specifically to the concept of classes. Prototype and Builder are the patterns that do not rely specifically on the class concept by definition. In fact, JavaScript implements both natively:
The prototype pattern, through the native
Object
. See on the ECMA 5.1 specification:// prototype is a fully initialized instance of Object. var prototype = { // ... }; // create a new object with specified prototype var object = Object.create( prototype );
The builder pattern, allowing the use of a function as an object's ~~constructor~~ when invoked with the
new
keyword (aka. constructor pattern). As said previously, the use of thenew
keyword should be completely eliminated so this pattern should be avoided.// the function builder separates object construction from its representation (aka instance) function builder(){ // ... } builder.prototype = { // ... }; // builder can create different representations with the same construction process var object = new builder();
~~prototypal~~ ~~classical~~ practical inheritance
As said, the tools to implement a prototypal inheritance pattern, where object instances are created from other object instances, are natively bundled within ECMAScript specification. Object.create
is the way to create a new object with the specified prototype. The challengue is a pattern that efficiently replaces the ~~constructor~~ builder pattern on the task of initialize a newly created object with the specified prototype. The solution is quite simple, and surprisingly somewhere between the constructor pattern and the prototypal inheritance pattern: use a function that creates a new object with speficied prototype, performs all neccessary initializations, and returns the newly created instance.
function builder(){
var instance = Object.create(builder.prototype);
// initialize the instance...
return instance;
}
builder.prototype = {
// ...
};
var object = builder();
The code above will pass an instanceof
check as expected. Any object
having builder.prototype
on its prototype chain will resolve true
for object instanceof builder
. See on the ECMAScript 5.1 specification:
The big concern now is how to implement the inheritance chain, where objects created through builders inherit from other objects created through builders. Each derived object has to perform ancestor's initializing routines too so is needed a mechanism with prototypes inheriting from ancestor prototypes while builders internally call the ancestor builders. The key is to ensure that the oldest ancestor on the chain will create the new object specifing the child's prototype. There are many solutions, but the simpler is execute builders within the context of the desired prototypes.
function Grandpa(){
var instance = Object.create(this);
// initialize the instance...
return instance;
}
Grandpa.prototype = {
// ...
};
function Parent(){
var instance = Grandpa.call(this);
// initialize the instance...
return instance;
}
Parent.prototype = Object.create( Grandpa.prototype );
// Parent.prototype.x = ...
// ...
function Child(){
var instance = Parent.call(this);
// initialize the instance...
return instance;
}
Child.prototype = Object.create( Parent.prototype );
// Child.prototype.x = ...
// ...
var grandpa = Grandpa.call( Grandpa.prototype );
var parent = Parent.call( Parent.prototype );
var child = Child.call( Child.prototype );
As seen above, now both the declaration and the creation of new objects becomes unnecesarily verbose. That's reason enough to use a helper function. In fact two functions are needed, one to extend prototypes and another to wrap builders to ensure they are executed within a proper context.
function extend( prototype, extension ){
var object = Object.create( prototype );
for( var property in extension ){
if( extension.hasOwnProperty(property) ){
object[property] = extension[property];
}
}
return object;
};
function builder( builder, prototype, extension ){
if( extension ){
prototype = extend( prototype, extension );
}
function builderWrap(){
if( prototype.isPrototypeOf(this) ){
var context = this;
}
return builder.apply( context | prototype, arguments );
};
builderWrap.prototype = prototype;
return builderWrap;
};
The extend
function creates a new object with the specified prototype
and defines on it as many properties as the own enumerable properties that the extension
object has.
The builder
function creates a function that will apply to builder
the proper context, being the current context (this
) if it's an object inheriting from prototype
or prototype
elsecase. Additionally, any object who has prototype
on its prototype chain will pass an instanceof
check against the returned function. Optionaly provides acces to extend
functionalities: If extension is given, use as prototype
the result of extending prototype
with extension
.
With the help of this tools, the previous example can be rewrited as follows:
var Grandpa = builder(function(){
var instance = Object.create(this);
// initialize the instance...
return instance;
}, {
// Grandpa.prototype...
});
var Parent = builder(function(){
var instance = Grandpa.call(this);
// initialize the instance...
return instance;
}, Grandpa.prototype, {
// Parent.prototype ...
});
var Child = builder(function(){
var instance = Parent.call(this);
// initialize the instance...
return instance;
}, Parent.prototype, {
// Child.prototype ...
});
var grandpa = Grandpa();
var parent = Parent();
var child = Child();
That's all, the target is accomplished. There is no need of classes and no need to use the new keyword. This pattern decouples the creation and initialization of objects from its representation while maintains a true prototypal inheritance pattern. The mechanism to check instance types is the instanceof
operator.
When initialization is not needed, there is also a mechanism to inherit one object from another:
var Grandpa = {
// ...
};
var Parent = extend( Grandpa, {
// ...
});
var Child = extend( Parent, {
// ...
});
var grandpa = Object.create(Grandpa);
var parent = Object.create(Parent);
var child = Object.create(Child);