John Resig (of jQuery fame) provides a concise implementation of Simple JavaScript Inheritance. His approach inspired my attempt to improve things even further. I've rewritten Resig's original Class.extend
function to include the following advantages:
Performance – less overhead during class definition, object construction, and base class method calls
Flexibility – optimized for newer ECMAScript 5-patible browsers (e.g. Chrome), but provides equivalent "shim" for older browsers (e.g. IE6)
Compatibility – validates in strict mode and provides better tool patibility (e.g. VSDoc/JSDoc ments, Visual Studio IntelliSense, etc.)
Simplicity – you don't have to be a "ninja" to understand the source code (and it's even simpler if you lose the ECMAScript 5 features)
Robustness – passes more "corner case" unit tests (e.g. overriding toString in IE)
Because it almost seems too good to be true, I want to ensure my logic doesn't have any fundamental flaws or bugs, and see if anyone can suggest improvements or refute the code. With that, I present the classify
function:
function classify(base, properties)
{
/// <summary>Creates a type (i.e. class) that supports prototype-chaining (i.e. inheritance).</summary>
/// <param name="base" type="Function" optional="true">The base class to extend.</param>
/// <param name="properties" type="Object" optional="true">The properties of the class, including its constructor and members.</param>
/// <returns type="Function">The class.</returns>
// quick-and-dirty method overloading
properties = (typeof(base) === "object") ? base : properties || {};
base = (typeof(base) === "function") ? base : Object;
var basePrototype = base.prototype;
var derivedPrototype;
if (Object.create)
{
// allow newer browsers to leverage ECMAScript 5 features
var propertyNames = Object.getOwnPropertyNames(properties);
var propertyDescriptors = {};
for (var i = 0, p; p = propertyNames[i]; i++)
propertyDescriptors[p] = Object.getOwnPropertyDescriptor(properties, p);
derivedPrototype = Object.create(basePrototype, propertyDescriptors);
}
else
{
// provide "shim" for older browsers
var baseType = function() {};
baseType.prototype = basePrototype;
derivedPrototype = new baseType;
// add enumerable properties
for (var p in properties)
if (properties.hasOwnProperty(p))
derivedPrototype[p] = properties[p];
// add non-enumerable properties (see )
if (!{ constructor: true }.propertyIsEnumerable("constructor"))
for (var i = 0, a = [ "constructor", "hasOwnProperty", "isPrototypeOf", "propertyIsEnumerable", "toLocaleString", "toString", "valueOf" ], p; p = a[i]; i++)
if (properties.hasOwnProperty(p))
derivedPrototype[p] = properties[p];
}
// build the class
var derived = properties.hasOwnProperty("constructor") ? properties.constructor : function() { base.apply(this, arguments); };
derived.prototype = derivedPrototype;
derived.prototype.constructor = derived;
derived.prototype.base = derived.base = basePrototype;
return derived;
}
And the usage is nearly identical to Resig's except for the constructor name (constructor
vs. init
) and the syntax for base class method calls.
/* Example 1: Define a minimal class */
var Minimal = classify();
/* Example 2a: Define a "plain old" class (without using the classify function) */
var Class = function()
{
this.name = "John";
};
Class.prototype.count = function()
{
return this.name + ": One. Two. Three.";
};
/* Example 2b: Define a derived class that extends a "plain old" base class */
var SpanishClass = classify(Class,
{
constructor: function()
{
this.name = "Juan";
},
count: function()
{
return this.name + ": Uno. Dos. Tres.";
}
});
/* Example 3: Define a Person class that extends Object by default */
var Person = classify(
{
constructor: function(name, isQuiet)
{
this.name = name;
this.isQuiet = isQuiet;
},
canSing: function()
{
return !this.isQuiet;
},
sing: function()
{
return this.canSing() ? "Figaro!" : "Shh!";
},
toString: function()
{
return "Hello, " + this.name + "!";
}
});
/* Example 4: Define a Ninja class that extends Person */
var Ninja = classify(Person,
{
constructor: function(name, skillLevel)
{
Ninja.base.constructor.call(this, name, true);
this.skillLevel = skillLevel;
},
canSing: function()
{
return Ninja.base.canSing.call(this) || this.skillLevel > 200;
},
attack: function()
{
return "Chop!";
}
});
/* Example 4: Define an ExtremeNinja class that extends Ninja that extends Person */
var ExtremeNinja = classify(Ninja,
{
attack: function()
{
return "Chop! Chop!";
},
backflip: function()
{
this.skillLevel++;
return "Woosh!";
}
});
var m = new Minimal();
var c = new Class();
var s = new SpanishClass();
var p = new Person("Mary", false);
var n = new Ninja("John", 100);
var e = new ExtremeNinja("World", 200);
And here are my QUnit tests which all pass:
equals(m instanceof Object && m instanceof Minimal && m.constructor === Minimal, true);
equals(c instanceof Object && c instanceof Class && c.constructor === Class, true);
equals(s instanceof Object && s instanceof Class && s instanceof SpanishClass && s.constructor === SpanishClass, true);
equals(p instanceof Object && p instanceof Person && p.constructor === Person, true);
equals(n instanceof Object && n instanceof Person && n instanceof Ninja && n.constructor === Ninja, true);
equals(e instanceof Object && e instanceof Person && e instanceof Ninja && e instanceof ExtremeNinja && e.constructor === ExtremeNinja, true);
equals(c.count(), "John: One. Two. Three.");
equals(s.count(), "Juan: Uno. Dos. Tres.");
equals(p.isQuiet, false);
equals(p.canSing(), true);
equals(p.sing(), "Figaro!");
equals(n.isQuiet, true);
equals(n.skillLevel, 100);
equals(n.canSing(), false);
equals(n.sing(), "Shh!");
equals(n.attack(), "Chop!");
equals(e.isQuiet, true);
equals(e.skillLevel, 200);
equals(e.canSing(), false);
equals(e.sing(), "Shh!");
equals(e.attack(), "Chop! Chop!");
equals(e.backflip(), "Woosh!");
equals(e.skillLevel, 201);
equals(e.canSing(), true);
equals(e.sing(), "Figaro!");
equals(e.toString(), "Hello, World!");
Does anyone see anything wrong with my approach vs. John Resig's original approach? Suggestions and feedback are wele!
NOTE: The above code has been modified significantly since I initially posted this question. The above represents the latest version. To see how it has evolved, please check the revision history.
John Resig (of jQuery fame) provides a concise implementation of Simple JavaScript Inheritance. His approach inspired my attempt to improve things even further. I've rewritten Resig's original Class.extend
function to include the following advantages:
Performance – less overhead during class definition, object construction, and base class method calls
Flexibility – optimized for newer ECMAScript 5-patible browsers (e.g. Chrome), but provides equivalent "shim" for older browsers (e.g. IE6)
Compatibility – validates in strict mode and provides better tool patibility (e.g. VSDoc/JSDoc ments, Visual Studio IntelliSense, etc.)
Simplicity – you don't have to be a "ninja" to understand the source code (and it's even simpler if you lose the ECMAScript 5 features)
Robustness – passes more "corner case" unit tests (e.g. overriding toString in IE)
Because it almost seems too good to be true, I want to ensure my logic doesn't have any fundamental flaws or bugs, and see if anyone can suggest improvements or refute the code. With that, I present the classify
function:
function classify(base, properties)
{
/// <summary>Creates a type (i.e. class) that supports prototype-chaining (i.e. inheritance).</summary>
/// <param name="base" type="Function" optional="true">The base class to extend.</param>
/// <param name="properties" type="Object" optional="true">The properties of the class, including its constructor and members.</param>
/// <returns type="Function">The class.</returns>
// quick-and-dirty method overloading
properties = (typeof(base) === "object") ? base : properties || {};
base = (typeof(base) === "function") ? base : Object;
var basePrototype = base.prototype;
var derivedPrototype;
if (Object.create)
{
// allow newer browsers to leverage ECMAScript 5 features
var propertyNames = Object.getOwnPropertyNames(properties);
var propertyDescriptors = {};
for (var i = 0, p; p = propertyNames[i]; i++)
propertyDescriptors[p] = Object.getOwnPropertyDescriptor(properties, p);
derivedPrototype = Object.create(basePrototype, propertyDescriptors);
}
else
{
// provide "shim" for older browsers
var baseType = function() {};
baseType.prototype = basePrototype;
derivedPrototype = new baseType;
// add enumerable properties
for (var p in properties)
if (properties.hasOwnProperty(p))
derivedPrototype[p] = properties[p];
// add non-enumerable properties (see https://developer.mozilla/en/ECMAScript_DontEnum_attribute)
if (!{ constructor: true }.propertyIsEnumerable("constructor"))
for (var i = 0, a = [ "constructor", "hasOwnProperty", "isPrototypeOf", "propertyIsEnumerable", "toLocaleString", "toString", "valueOf" ], p; p = a[i]; i++)
if (properties.hasOwnProperty(p))
derivedPrototype[p] = properties[p];
}
// build the class
var derived = properties.hasOwnProperty("constructor") ? properties.constructor : function() { base.apply(this, arguments); };
derived.prototype = derivedPrototype;
derived.prototype.constructor = derived;
derived.prototype.base = derived.base = basePrototype;
return derived;
}
And the usage is nearly identical to Resig's except for the constructor name (constructor
vs. init
) and the syntax for base class method calls.
/* Example 1: Define a minimal class */
var Minimal = classify();
/* Example 2a: Define a "plain old" class (without using the classify function) */
var Class = function()
{
this.name = "John";
};
Class.prototype.count = function()
{
return this.name + ": One. Two. Three.";
};
/* Example 2b: Define a derived class that extends a "plain old" base class */
var SpanishClass = classify(Class,
{
constructor: function()
{
this.name = "Juan";
},
count: function()
{
return this.name + ": Uno. Dos. Tres.";
}
});
/* Example 3: Define a Person class that extends Object by default */
var Person = classify(
{
constructor: function(name, isQuiet)
{
this.name = name;
this.isQuiet = isQuiet;
},
canSing: function()
{
return !this.isQuiet;
},
sing: function()
{
return this.canSing() ? "Figaro!" : "Shh!";
},
toString: function()
{
return "Hello, " + this.name + "!";
}
});
/* Example 4: Define a Ninja class that extends Person */
var Ninja = classify(Person,
{
constructor: function(name, skillLevel)
{
Ninja.base.constructor.call(this, name, true);
this.skillLevel = skillLevel;
},
canSing: function()
{
return Ninja.base.canSing.call(this) || this.skillLevel > 200;
},
attack: function()
{
return "Chop!";
}
});
/* Example 4: Define an ExtremeNinja class that extends Ninja that extends Person */
var ExtremeNinja = classify(Ninja,
{
attack: function()
{
return "Chop! Chop!";
},
backflip: function()
{
this.skillLevel++;
return "Woosh!";
}
});
var m = new Minimal();
var c = new Class();
var s = new SpanishClass();
var p = new Person("Mary", false);
var n = new Ninja("John", 100);
var e = new ExtremeNinja("World", 200);
And here are my QUnit tests which all pass:
equals(m instanceof Object && m instanceof Minimal && m.constructor === Minimal, true);
equals(c instanceof Object && c instanceof Class && c.constructor === Class, true);
equals(s instanceof Object && s instanceof Class && s instanceof SpanishClass && s.constructor === SpanishClass, true);
equals(p instanceof Object && p instanceof Person && p.constructor === Person, true);
equals(n instanceof Object && n instanceof Person && n instanceof Ninja && n.constructor === Ninja, true);
equals(e instanceof Object && e instanceof Person && e instanceof Ninja && e instanceof ExtremeNinja && e.constructor === ExtremeNinja, true);
equals(c.count(), "John: One. Two. Three.");
equals(s.count(), "Juan: Uno. Dos. Tres.");
equals(p.isQuiet, false);
equals(p.canSing(), true);
equals(p.sing(), "Figaro!");
equals(n.isQuiet, true);
equals(n.skillLevel, 100);
equals(n.canSing(), false);
equals(n.sing(), "Shh!");
equals(n.attack(), "Chop!");
equals(e.isQuiet, true);
equals(e.skillLevel, 200);
equals(e.canSing(), false);
equals(e.sing(), "Shh!");
equals(e.attack(), "Chop! Chop!");
equals(e.backflip(), "Woosh!");
equals(e.skillLevel, 201);
equals(e.canSing(), true);
equals(e.sing(), "Figaro!");
equals(e.toString(), "Hello, World!");
Does anyone see anything wrong with my approach vs. John Resig's original approach? Suggestions and feedback are wele!
NOTE: The above code has been modified significantly since I initially posted this question. The above represents the latest version. To see how it has evolved, please check the revision history.
Share Improve this question edited Aug 6, 2011 at 2:22 munity wiki27 revs
Will 2
-
I would remend
Object.create
and traitsjs. Inheritance is no good in javascript, use object position – Raynos Commented Jul 27, 2011 at 21:19 - Maybe I am just not used to it yet, but the syntax of traits makes my head spin. I think I will pass until it gains a following... – Will Commented Jul 28, 2011 at 5:20
3 Answers
Reset to default 5Some time ago, I looked at several object systems for JS and even implemented a few of my own, eg class.js (ES5 version) and proto.js.
The reason why I never used them: you'll end up writing the same amount of code. Case in point: Resig's Ninja-example (only added some whitespace):
var Person = Class.extend({
init: function(isDancing) {
this.dancing = isDancing;
},
dance: function() {
return this.dancing;
}
});
var Ninja = Person.extend({
init: function() {
this._super(false);
},
swingSword: function() {
return true;
}
});
19 lines, 264 bytes.
Standard JS with Object.create()
(which is an ECMAScript 5 function, but for our purposes can be replaced by a custom ES3 clone()
implementation):
function Person(isDancing) {
this.dancing = isDancing;
}
Person.prototype.dance = function() {
return this.dancing;
};
function Ninja() {
Person.call(this, false);
}
Ninja.prototype = Object.create(Person.prototype);
Ninja.prototype.swingSword = function() {
return true;
};
17 lines, 282 bytes. Imo, the extra bytes are not really woth the added plexity of a seperate object system. It's easy enough to make the standard example shorter by adding some custom functions, but again: it's not realy worth it.
Not so fast. It just doesn't work.
Consider:
var p = new Person(true);
alert("p.dance()? " + p.dance()); => true
var n = new Ninja();
alert("n.dance()? " + n.dance()); => false
n.dancing = true;
alert("n.dance()? " + n.dance()); => false
base
is just another object initialized with default members that made you think it works.
EDIT: for the record, here is my own (albeit more verbose) implementation of Java like inheritance in Javascript, crafted in 2006 at the time I got inspired by Dean Edward's Base.js (and I agree with him when he says John's version is just a rewrite of his Base.js). You can see it in action (and step debug it in Firebug) here.
/**
* A function that does nothing: to be used when resetting callback handlers.
* @final
*/
EMPTY_FUNCTION = function()
{
// does nothing.
}
var Class =
{
/**
* Defines a new class from the specified instance prototype and class
* prototype.
*
* @param {Object} instancePrototype the object literal used to define the
* member variables and member functions of the instances of the class
* being defined.
* @param {Object} classPrototype the object literal used to define the
* static member variables and member functions of the class being
* defined.
*
* @return {Function} the newly defined class.
*/
define: function(instancePrototype, classPrototype)
{
/* This is the constructor function for the class being defined */
var base = function()
{
if (!this.__prototype_chaining
&& base.prototype.initialize instanceof Function)
base.prototype.initialize.apply(this, arguments);
}
base.prototype = instancePrototype || {};
if (!base.prototype.initialize)
base.prototype.initialize = EMPTY_FUNCTION;
for (var property in classPrototype)
{
if (property == 'initialize')
continue;
base[property] = classPrototype[property];
}
if (classPrototype && (classPrototype.initialize instanceof Function))
classPrototype.initialize.apply(base);
function augment(method, derivedPrototype, basePrototype)
{
if ( (method == 'initialize')
&&(basePrototype[method].length == 0))
{
return function()
{
basePrototype[method].apply(this);
derivedPrototype[method].apply(this, arguments);
}
}
return function()
{
this.base = function()
{
return basePrototype[method].apply(this, arguments);
};
return derivedPrototype[method].apply(this, arguments);
delete this.base;
}
}
/**
* Provides the definition of a new class that extends the specified
* <code>parent</code> class.
*
* @param {Function} parent the class to be extended.
* @param {Object} instancePrototype the object literal used to define
* the member variables and member functions of the instances of the
* class being defined.
* @param {Object} classPrototype the object literal used to define the
* static member variables and member functions of the class being
* defined.
*
* @return {Function} the newly defined class.
*/
function extend(parent, instancePrototype, classPrototype)
{
var derived = function()
{
if (!this.__prototype_chaining
&& derived.prototype.initialize instanceof Function)
derived.prototype.initialize.apply(this, arguments);
}
parent.prototype.__prototype_chaining = true;
derived.prototype = new parent();
delete parent.prototype.__prototype_chaining;
for (var property in instancePrototype)
{
if ( (instancePrototype[property] instanceof Function)
&&(parent.prototype[property] instanceof Function))
{
derived.prototype[property] = augment(property, instancePrototype, parent.prototype);
}
else
derived.prototype[property] = instancePrototype[property];
}
derived.extend = function(instancePrototype, classPrototype)
{
return extend(derived, instancePrototype, classPrototype);
}
for (var property in classPrototype)
{
if (property == 'initialize')
continue;
derived[property] = classPrototype[property];
}
if (classPrototype && (classPrototype.initialize instanceof Function))
classPrototype.initialize.apply(derived);
return derived;
}
base.extend = function(instancePrototype, classPrototype)
{
return extend(base, instancePrototype, classPrototype);
}
return base;
}
}
And this is how you use it:
var Base = Class.define(
{
initialize: function(value) // Java constructor equivalent
{
this.property = value;
},
property: undefined, // member variable
getProperty: function() // member variable accessor
{
return this.property;
},
foo: function()
{
alert('inside Base.foo');
// do something
},
bar: function()
{
alert('inside Base.bar');
// do something else
}
},
{
initialize: function() // Java static initializer equivalent
{
this.property = 'Base';
},
property: undefined, // static member variables can have the same
// name as non static member variables
getProperty: function() // static member functions can have the same
{ // name as non static member functions
return this.property;
}
});
var Derived = Base.extend(
{
initialize: function()
{
this.base('derived'); // chain with parent class's constructor
},
property: undefined,
getProperty: function()
{
return this.property;
},
foo: function() // override foo
{
alert('inside Derived.foo');
this.base(); // call parent class implementation of foo
// do some more treatments
}
},
{
initialize: function()
{
this.property = 'Derived';
},
property: undefined,
getProperty: function()
{
return this.property;
}
});
var b = new Base('base');
alert('b instanceof Base returned: ' + (b instanceof Base));
alert('b.getProperty() returned: ' + b.getProperty());
alert('Base.getProperty() returned: ' + Base.getProperty());
b.foo();
b.bar();
var d = new Derived('derived');
alert('d instanceof Base returned: ' + (d instanceof Base));
alert('d instanceof Derived returned: ' + (d instanceof Derived));
alert('d.getProperty() returned: ' + d.getProperty());
alert('Derived.getProperty() returned: ' + Derived.getProperty());
d.foo();
d.bar();
This is about as simple as you can get. It was taken from http://www.sitepoint./javascript-inheritance/.
// copyPrototype is used to do a form of inheritance. See http://www.sitepoint./blogs/2006/01/17/javascript-inheritance/#
// Example:
// function Bug() { this.legs = 6; }
// Insect.prototype.getInfo = function() { return "a general insect"; }
// Insect.prototype.report = function() { return "I have " + this.legs + " legs"; }
// function Millipede() { this.legs = "a lot of"; }
// copyPrototype(Millipede, Bug); /* Copy the prototype functions from Bug into Millipede */
// Millipede.prototype.getInfo = function() { return "please don't confuse me with a centipede"; } /* ''Override" getInfo() */
function copyPrototype(descendant, parent) {
var sConstructor = parent.toString();
var aMatch = sConstructor.match(/\s*function (.*)\(/);
if (aMatch != null) { descendant.prototype[aMatch[1]] = parent; }
for (var m in parent.prototype) {
descendant.prototype[m] = parent.prototype[m];
}
};