Javascript Inheritance
From Ajax Patterns
Contents |
Ajax/Javascript Programming and Usability in "Ajax Design Patterns" Book |
Background
As web applications become more dynamic and responsive to the user, they tend to use an increasing amount of client-side JavaScript. As a result, many web developers have sought ways to apply object oriented techniques to their JavaScript in order to manage its increasing complexity. Since JavaScript is not a classical object oriented language, a number of different approaches have been crafted over the years to pull off various levels of object oriented behavior. Below you find a listing of different patterns that have been seen in use throughout the web development community.
Each pattern has been given a unique name so that developers will have common terminology by which to refer to them. Additionally, a working code example of each pattern is provided along with any benefits or negatives. Some of the patterns are more complex than others and therefore require supporting code. In those cases, the supporting code will be included in a separate code section above the example so that the example can remain focused on the usage of the pattern.
Prototype Inheritance
This is probably the most common form of JavaScript class definition and inheritance. It's not really much of a pattern because this is essentially the approach that was bullt into the language by its creators. The 'prototype' member exists on all JavaScript objects and since all functions in the language are object instances, you can access the prototype member of any function that has been defined.
Benefits
- Ahead of time class definition
- Natively supported by language
- Fast object instance creation
- Functions and properties defined once per prototype, not once per object
Negatives
- Fragmented syntax for class definition
- Verbose due to duplicate class name references
- Visually appears more procedural/functional than object oriented
Example
function BaseClass() {
//BaseClass constructor code goes here
}
BaseClass.prototype.getName = function() {
return "BaseClass";
}
function SubClass() {
//SubClass constructor code goes here
}
//Inherit the methods of BaseClass
SubClass.prototype = new BaseClass();
//Override the parent's getName method
SubClass.prototype.getName = function() {
return "SubClass";
}
//Alerts "SubClass"
alert(new SubClass().getName());
Closure Inheritance
Unlike the approach taken by Prototype Inheritance, this approach leverages a feature of JavaScript known as closures in order to assemble the methods and properites of an object during its construction. A closure is essentially the ability to define an inner function within another function that can access the variables and functions defined within the scope it was created. While closures are a powerful language construct, there are numerous performance and memory considerations that must be taken into account when developing Ajax applications. In particular, the automatic garbage collector in the IE browser has a major flaw that prevents it from collecting DOM objects that are referenced from within a closure. Therefore, it is generally recommended that you avoid this approach in your client-side Ajax programming.
Benefits
- Fairly clean syntax for class definition
- Natively supported by language
- Concise, eliminates duplication in definition
- Supports differences in class definition between instances
- Only way to establish methods and properties that are truely private.
Negatives
- Run time class definition is slower
- Slow object instance creation
- May result in SEVERE browser memory leaks caused by closures interacting with DOM objects. Mainly an issue with IE although some issues do exist in other browsers.
Example
function BaseClass() {
this.getName = function() {
return "BaseClass";
};
//BaseClass constructor code goes here
}
function SubClass() {
//Override the parent's getName method
this.getName = function() {
return "SubClass";
}
//SubClass constructor code goes here
}
//Inherit the methods of BaseClass
SubClass.prototype = new BaseClass();
//Alerts "SubClass"
alert(new SubClass().getName());
Direct Inheritance (ThinWire Ajax Framework)
Supporting Code
//Defines the top level Class
function Class() { }
Class.prototype.construct = function() {};
Class.extend = function(def) {
var classDef = function() {
if (arguments[0] !== Class) { this.construct.apply(this, arguments); }
};
var proto = new this(Class);
var superClass = this.prototype;
for (var n in def) {
var item = def[n];
if (item instanceof Function) item.$ = superClass;
proto[n] = item;
}
classDef.prototype = proto;
//Give this new class the same static extend method
classDef.extend = this.extend;
return classDef;
};
Example
var BaseClass = Class.extend({
construct: function() {
//BaseClass constructor code goes here
},
getName: function() {
return "BaseClass(" + this.getId() + ")";
},
getId: function() {
return 1;
}
});
var SubClass = BaseClass.extend({
construct: function() {
//SubClass constructor code goes here
},
//Override BaseClass's getName method
getName: function() {
return "SubClass(" + this.getId() + ") extends " +
arguments.callee.$.getName.call(this);
},
//Override BaseClass's getId method
getId: function() {
return 2;
}
});
var TopClass = SubClass.extend({
//Override SubClass's getName method
getName: function() {
//Calls the getName() method of SubClass
return "TopClass(" + this.getId() + ") extends " +
arguments.callee.$.getName.call(this);
},
//Override SubClass's getId method
getId: function() {
//Just like the last example, this.getId()
//always returns the proper value of 2.
return arguments.callee.$.getId.call(this);
}
});
//Alerts "TopClass(2) extends SubClass(2) extends BaseClass(2)"
alert(new TopClass().getName());
Bound Inheritance (ThinWire Ajax Framework)
Supporting Code
function Class() { }
Class.prototype.construct = function() {};
Class.__asMethod__ = function(func, superClass) {
return function() {
var currentSuperClass = this.$;
this.$ = superClass;
var ret = func.apply(this, arguments);
this.$ = currentSuperClass;
return ret;
};
};
Class.extend = function(def) {
var classDef = function() {
if (arguments[0] !== Class) { this.construct.apply(this, arguments); }
};
var proto = new this(Class);
var superClass = this.prototype;
for (var n in def) {
var item = def[n];
if (item instanceof Function) {
item = Class.__asMethod__(item, superClass);
}
proto[n] = item;
}
proto.$ = superClass;
classDef.prototype = proto;
classDef.extend = this.extend;
return classDef;
};
Example
//Hey, this class definition approach
//looks much cleaner than then others.
var BaseClass = Class.extend({
construct: function() {
//BaseClass constructor code goes here
},
getName: function() {
return "BaseClass(" + this.getId() + ")";
},
getId: function() {
return 1;
}
});
var SubClass = BaseClass.extend({
construct: function() {
//SubClass constructor code goes here
},
//Override BaseClass's getName method
getName: function() {
return "SubClass(" + this.getId() + ") extends " +
this.$.getName.call(this);
},
//Override BaseClass's getId method
getId: function() {
return 2;
}
});
var TopClass = SubClass.extend({
construct: function() {
//TopClass constructor code goes here
},
//Override SubClass's getName method
getName: function() {
return "TopClass(" + this.getId() + ") extends " +
this.$.getName.call(this);
},
//Override SubClass's getId method
getId: function() {
return this.$.getId.call(this);
}
});
//Alerts "TopClass(2) extends SubClass(2) extends BaseClass(2)"
alert(new TopClass().getName());
Reference Inheritance (Douglas Crockford) [BROKEN]
Based on the standard Prototype Inheritance approach and named for it's super class reference walking, this general approach became widely known after Douglas Crockford published his paper entitled link Classical Inheritance in JavaScript. Since then a number of similar approaches have appeared as an increasing number of developers have a sought a way to handle super-class method calling in JavaScript. Approaches like this are popular and widely spread because on the surface they appear to work correctly. However, these approaches tend to break down when the method call stack goes deeper than one super-class call. The following example demonstrates this failure:
Supporting Code
Function.prototype.inherits = function(parent) {
var d = 0, p = (this.prototype = new parent());
this.prototype.uber = function(name) {
var f, r, t = d, v = parent.prototype;
if (t) {
while (t) {
v = v.constructor.prototype;
t -= 1;
}
f = v[name];
} else {
f = p[name];
if (f == this[name]) {
f = v[name];
}
}
d += 1;
r = f.apply(this, Array.prototype.slice.apply(arguments, [1]));
d -= 1;
return r;
};
};
Example
function BaseClass() {
//BaseClass constructor code goes here
}
BaseClass.prototype.getName = function() {
return "BaseClass(" + this.getId() + ")";
}
BaseClass.prototype.getId = function() {
return 1;
}
function SubClass() {
//SubClass constructor code goes here
}
//Inherit the methods of BaseClass
//and the 'uber' super calling method
SubClass.inherits(BaseClass);
//Override the parent's getName method
SubClass.prototype.getName = function() {
return "SubClass(" + this.getId() + ") extends " +
this.uber("getName");
}
//Override the parent's getId method
SubClass.prototype.getId = function() {
return 2;
}
function TopClass() {
//TopClass constructor code goes here
}
//Inherit the methods of SubClass
//and the 'uber' super calling method
TopClass.inherits(SubClass);
TopClass.prototype.getName = function() {
return "TopClass(" + this.getId() + ") extends " +
this.uber("getName");
}
TopClass.prototype.getId = function() {
return this.uber("getId");
}
//INCORRECT: Alerts "TopClass(2) extends SubClass(1) extends BaseClass(1)"
alert(new TopClass().getName());
But there is a patched version (Note: for users which can't access blogspot (from Mainland China, India, Pakistan and Iran), please visit inblogs mirror)
Patched Code
Function.prototype.inherits = function(parent) {
var d = {}, p = (this.prototype = new parent());
this.prototype.uber = function(name) {
if (!(name in d)) d[name] = 0;
var f, r, t = d[name], v = parent.prototype;
if (t) {
while (t) {
v = v.constructor.prototype;
t -= 1;
}
f = v[name];
} else {
f = p[name];
if (f == this[name]) {
f = v[name];
}
}
d[name] += 1;
r = f.apply(this, Array.prototype.slice.apply(arguments, [1]));
d[name] -= 1;
return r;
}
}
The problem is because of d, which indicates the depth of supercall. In original code, every method share the same d, which will cause bug when nest call (one method call another). So give every method their own counter, solves the problem.
Lazy Inheritance (JSINER)
"Lazy inheritance" is a design pattern used in JavaScript computer programming. It designates a postponed linking of an object with it's prototype (class) until it is needed. If used properly, such approach may increase efficiency, simplicity and flexibility of OOP based code written using JavaScript. Unlike of "classical" approach, it supports a "Lazy" way of inheritance implementation, since reference to parent class is invoked only at the moment of object instance creation. Basically, it's possible to say that instances of objects in such approach are created in "mixed" mode – on first invocation, an appropriate factory is used to modify object constructor prototype which is later used for subsequent object instances creation. Moreover, since "Lazy inheritance" is called only once at the moment of first object instance creation, it becomes possible to combine process of class prototype creation with resolving necessary dependencies of that class. In other words, the process of prototype construction also allows loading scripts (if ones were not loaded before) which particular class depends on.
Benefits
Such approach to inheritance support has the following benefits:
- Lazy inheritance has the same benefits as classical JavaScript prototype features;
- It is not necessary to maintain proper order of script files declaration;
- In most cases, HTML page is loaded faster since there are no objects created during page initialization and since some scripts could be loaded only at the moment in which they are actually necessary (they are necessary if the caller code during it's execution really creates instances of corresponding classes);
- Dependencies are declared in more explicit way and class depends only on concrete scripts; Moreover, it's not necessary to artificially group particular scripts into "modules" and define dependencies between such "modules";
- If lazy mode of scripts loading is used, only necessary scripts (which are actually required for application functionality) will be loaded. If, for example, some HTML page may operate in different modes, like "read-write" and "read-only", for every mode required scripts will be loaded and this will be performed automatically without manual script loading optimization.
Negatives
- uses synchronous xmlhttp which really, really is a bad thing to the browser UI (if a lot of scripts need to be loaded and or size of javascript files)
Lazy Inheritance Internals
Lazy inheritance is invoked within the Object' constructor code and is called only once for first object instance creation. After invocation, the following steps:
- First, the algorithm tests Object' dependencies;
- If Object' dependency had been registered before the object constructor was called (interrelations can be defined in the object constructor too), corresponding JavaScript dependencies are loaded.
- Then, if the class, from which the object inherits, is defined and it is not a default one (Object), Object's constructor prototype is modified by classical schema of prototype-based inheritance.
- After completing prototype modification, constructor of the Object' is called again to apply changes to new instance of the Object.
The derived instance of the Object may be modified in the constructor according to particular logic. For example, some properties of the object may be defined. The resulting object is the same JavaScript object as it would be created with prototype-based inheritance.
Example
First, we define Person. Let's assume that declaration of that class is placed to person.js JavaScript file.
/**
* Constructor for Person class
*/
function Person(aName)
{
this.fName = aName;
}
/**
* Function which returns name of Person’s.
*/
Person.prototype.getName = function()
{
return this.fName;
};
/**
* Here we define function which returns string representation of Person
* assign it to prototype of Person class
*/
Person.prototype.toString = function()
{
return "Person: " + this.getName();
};
Here we assume that the code below is placed to employee.js file.
/*
* Here we define that Employee class depends on person script.
*/
JSINER.addDependency( {Employee:"person"} );
/**
* Here we define constructor for Employee class
* and declare that Employee class inherits Person one
*/
function Employee(aName, aUID)
{
var self = JSINER.extend(this, "Person");
self.fName = aName;
self.fUID = aUID;
return self;
}
/**
* Here we define function which return UID of employee
* and assign it to prototype of Employee class.
*/
Employee.prototype.getUID = function()
{
return this.fUID;
};
/**
* Here we define function which returns string representation of Employee
*/
Employee.prototype.toString = function()
{
var person = Employee.superClass.toString.call(this);
return this.getUID() + ":" + person;
};
And this is sample HTML page which uses declarations of JavaScript object above:
<html>
<head>
<title>Lazy inheritance example.</title>
// We need to include reference to JSNIER code
<script src="script/jsiner.js"></script>
// Note that we include reference to script that contains Employee class only
<script src="script/employee.js"></script>
// And no it's not required to reference person.js script explicitely -
// JSINER will resolve and load it automatically
</head>
<body>
<script>
var employee = new Employee('John Doe', 1212);
alert(employee);
</script>
</body>
</html>
During execution of example code listed above, linkage of Employee and Person as well as loading of necessary script "person.js" is performed automatically with the first creation of Employee instance.
As it is illustrated by examples above, "Lazy inheritance":
- It is called only once during first creation of object instance;
- Linkage of objects and scripts is performed automatically;
- To link classes the prototype based inheritance is used.
In general, the proposed scheme considers "lazy" loading as primary mode for loading particular scripts. However, this is not strict requirement – it is also possible to force loading of all scripts which form particular application before starting objects creation if this way of loading is required by particular application architecture.
Time your website with
WebWait - from the creator of AjaxPatterns.org
