Contents
Introduction
I read David Flanagan's blog entry asking for comments on a function, produced for instruction purposes, for managing constructors and prototypical inheritance.
His origial function is reproduced below:-
/**
* Class() -- a utility function for defining JavaScript classes.
*
* This function expects a single object as its only argument. It defines
* a new JavaScript class based on the data in that object and returns the
* constructor function of the new class. This function handles the repetitive
* tasks of defining classes: setting up the prototype object for correct
* inheritance, copying methods from other types, and so on.
*
* The object passed as an argument should have some or all of the
* following properties:
*
* name: the name of the class being defined.
* If specified, this value will be stored in the classname
* property of the returned constructor object.
*
* extend: The constructor of the class to be extended. The returned
* constructor automatically chains to this function. This value
* is stored in the superclass property of the constructor object.
*
* init: The initialization function for the class. If defined, the
* constructor will pass all of its arguments to this function.
*
* methods: An object that specifies the instance methods for the class.
* These functions are given an overrides property for chaining.
* They can call "chain(this, arguments)" to invoke the method
* they override. "arguments" must appear literally.
*
* statics: An object that specifies the static methods (and other static
* properties) for the class. The properties of this object become
* properties of the constructor function.
*
* mixins: A constructor function or array of constructor functions.
* The instance methods of each of the specified classes are copied
* into the prototype object of this new class so that the
* new class borrows the methods of each specified class.
* Mixins are processed in the order they are specified,
* so the methods of a mixin listed at the end of the array may
* overwrite the methods of those specified earlier. Note that
* methods specified in the methods object can override (and chain
* to) mixed-in methods.
*
* This function is named with a capital letter and looks like a constructor
* function. It can (but need not) be used with new: new Class({...})
**/
function Class(data) {
// Extract the fields we'll use from the argument object.
var extend = data.extend;
var init = data.init;
var classname = data.name || "Unnamed Class";
var statics = data.statics || {};
var mixins = data.mixins || [];
var methods = data.methods || {};
// Make a constructor function that chains to the superclass constructor
// and then calls the initialization method of this class.
// This will become the return value of this Class() method.
var constructor = function() {
if (extend) extend.apply(this, arguments); // Initialize superclass
if (init) init.apply(this, arguments); // Initialize ourself
};
// Copy static properties to the constructor function
for(var p in statics) constructor[p] = statics[p];
// Set superclass and classname properties of the constructor
constructor.superclass = extend || Object;
constructor.classname = classname;
// Create an instance of the superclass to use as the prototype for
// the new class. Assign it to constructor.prototype
var proto = constructor.prototype = new constructor.superclass();
// Delete any local properties of the prototype object
for(var p in proto)
if (proto.hasOwnProperty(p)) delete proto[p];
// Borrow methods from mixin classes by copying methods to our prototype.
if (!(mixins instanceof Array)) mixins = [mixins]; // Ensure an array
for(var i = 0; i < mixins.length; i++) { // For each mixin class
var m = mixins[i].prototype; // This is mixin prototype
for(var p in m) { // For each property of mixin
if (typeof m[p] == "function") { // If it is a function
proto[p] = m[p]; // Copy it to our prototype
}
}
}
// Copy instance methods to the prototype object
// This may override methods of the mixin classes or the superclass
for(var p in methods) { // For each name in methods object
var m = methods[p]; // This is the method to copy
if (typeof m == "function") { // If it is a function
m.overrides = proto[p]; // Remember anything it overrides
proto[p] = m; // Then store in the prototype
}
}
// All objects should know who their constructor was
proto.constructor = constructor;
// Finally, return the constructor function
return constructor;
}
/**
* This function is designed to be invoked with the this keyword as its
* 1st argument and the arguments array as its 2nd: chain(this, arguments).
* It uses the callee property to determine what function called this
* function, and then looks for an overrides property on that function.
* If it finds one, it invokes the overridden function on the object,
* passing the arguments
*/
function chain(o, args) {
m = args.callee; // The method that wants to chain
om = m.overrides // The method it overrides
if (om) return om.apply(o, args) // Invoke it, if it exists
}
// Example 9-11 demonstrates the methods above
//-----------------------------------------------
// A mixin class with a usefully generic equals() method for borrowing
var GenericEquals = Class({
name: "GenericEquals",
methods: {
equals: function(that) {
if (this == that) return true;
var propsInThat = 0;
for(var name in that) {
propsInThat++;
if (this[name] !== that[name]) return false;
}
// Now make sure that this object doesn't have additional props
var propsInThis = 0;
for(name in this) propsInThis++;
// If this has additional properties then they are not equal
if (propsInThis != propsInThat) return false;
// The two objects appear to be equal.
return true;
}
}
});
// A very simple Rectangle class
var Rectangle = Class({
name: "Rectangle",
init: function(w,h) {
this.width = w; this.height = h;
},
methods: {
area: function() { return this.width * this.height; },
compareTo: function(that) { return this.area() - that.area(); },
toString: function() {
return "[" + this.width + "," + this.height + "]"
}
}
});
// A subclass of Rectangle
var PositionedRectangle = Class({
name: "PositionedRectangle",
extend: Rectangle,
init: function(w,h,x,y) {
this.x = x;
this.y = y;
},
methods: {
isInside: function(x,y) {
return x > this.x && x < this.x+this.width &&
y > this.y && y < this.y+this.height;
},
toString: function() {
return chain(this,arguments) + "(" + this.x + "," + this.y + ")";
}
},
mixins: [GenericEquals]
});
var ColoredRectangle = new Class({
name: "ColoredRectangle",
extend: PositionedRectangle,
init: function(w,h,x,y,c) { this.c = c; },
methods: {
toString: function() { return chain(this,arguments) + ": " + this.c}
}
});
var cr = new ColoredRectangle(1,2,3,4,5);
alert(cr.toString()); // Demonstrate constructor and method chaining
I thought I would set out my comments here in a bit of detail.
I have produced an amended version of the function which you can try out further below. My amendments are numbered according to the comments below.
Comments
0. DontEnum
The proposal overlooked the DontEnum
bug in Internet Explorer, which is that if you use for .. in
to enumerate the properties of an Object
, IE will skip over any property whose name corresponds to a name on the original Object.prototype
in Section 15.4.2 of the ECMA Script 3 specification.
In other words, IE will not enumerate in a for .. in
loop the following:-
constructor NO
toString NO
valueOf NO
toLocaleString NO
prototype YES
isPrototypeOf NO
propertyIsEnumerable NO
hasOwnProperty NO
length YES
unique YES
The same I think applies if you enumerate properties on an Array
etc; but we are not interested in this, because we are only using for .. in
loops on an Object
in our case.
Because David's function uses a for .. in
loop, to loop through the methods and attach them to the prototype
object, it would not work if your new class were trying to override the toString
function on the Object.prototype
, because due to the bug in IE, the for .. in
loop would ignore any new method you defined called toString
.
For further information on the bug, see Mozilla Explanation.
Accordingly we need to explicily deal with toString
and valueOf
, by separately testing whether the data
object you supply has toString
defined on it using hasOwnProperty()
. If we just use data["toString"]
we have no way of telling whether it is returning Object.prototype.toString
or a new function we defined.
1. Local Variables
As there is a function expression within the code, I wondered whether the data
argument, and any local variables used to store elements of the data argument, might end up sticking around in memory as closures.
Perhaps this is a case of premature optimisation, but I amended the function so that it only uses the data
object, and does not create any local variables.
Following further feed-back there were two problems with this:-
- Firstly, amending the supplied
data
object could lead to errors if the function supplying thedata
object had a continuing use for it. Defensive programming might involve deep copying thedata
object, which could be expensive. - Secondly, some of the local variables (
extend
andinit
) were in fact needed as closures.
Accordingly my second version below avoids amending the data
object, but still sets to null
any local variables that are not needed in closures.
2. Mixin Constructor
I wondered whether it would be worth-while allowing the mixin
constructor functions to be called in the constructor
.
Overall this was considered too potentially problematic, partly because you cannot easily supply arguments to the mixin constructors, and partly because it might produce unintended consequences, if the mixin constructors were not designed for your class.
3. Inherit Statics
I wondered whether there was anything to be gained by allowing the constructor to inherit static properties and methods on the extend
object (and the mixin
constructors, for good measure).
Overall, there did not seem to be any good reason to do so, as the static properties were always available on those other classes.
4. Dummy prototype for mixins
The mixins are being added to the same prototype
object as the methods
. I wondered whether it might it be clearer to separate them into their own prototpe, using the dummy function method as seen on Douglas Crockford's page.
It may also be handy to have them on a separate prototype for chain
purposes, so that we can see where in the chain they sit.
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
I.e.
Object proto
^
mixinProto (for mixins)
^
Extend proto
^
mixinProto (for mixins)
^
Class proto
David felt that giving the mixins their own property in the prototype chain may give the impression that the whole mixin object is in fact part of the inheritance prototype chain, and detectable with instanceof
, which it is not.
More generally, David started to re-evaluate whether it was necessary to get involved with mixins at all, and his final version omits them.
From my point of view, when I re-considered my point, I don't think I was very clear in my own mind as to my reasons or the need, so I think my point was not a good one anyway.
5. Mixins in prototype chain
I wondered whether it be useful to consider further where the mixin
methods sit in the override
chain?
At present the choice is that if a mixin
method has the same name as a method on the extend
prototype chain, then the mixin
method overrides but does not chain to the extend
method, it chains to the mixin's
own override
chain.
There seemed to be many options here so I didn't know which was best to recommend. I supposed it would be up to the designer to understand what name conflicts might arise with mixins and plan for them.
The options I could see were:-
// PROPOSAL DOES THIS
[Class method >] mixin(later) > mixin(later) chain ...
// BIT COMPLICATED
[Class method >] mixin(later) > mixin(earler) > mixin(earlier) chain ...
// I SUGGEST THIS AS AN OPTION
[Class method >] mixin(later) > superclass(extend) chain ...
// BIT COMPLICATED
[Class method >] mixin(later) > mixin(earlier) > superclass(extend) chain ... // I INCLUDE THIS AS AN OPTION
// I ALSO SUGGEST THIS AS AN OPTION
// DON'T ALLOW MIXINS WHICH CONFLICT WITH SUPERCLASS'S PROTOTYPE
[Class method >] superclass prototype chain ...
David rightly pointed out that anything other than the first is going to be a source of problems.
The mixin functions will be part of their own inheritance chain, so to re-assign the overrides
property would destroy the mixin's own chain and again is not good defensive programming. The only alternative would be to deep copy the function, but then you will lose the original execution context of that funtion (e.g. any closures), and there would be the expense of a call to eval
or new Function
.
An additional sensible point was made that mixins, whilst sharing the same function name, may have an entirely different signature or purpose, and may be varied independently. Mixin's may be a convient way to re-use methods, but you do need to be careful and be clear what mixins you actually want to include.
6. Give functions a name
As the methods were all anonymous functions, I thought it might be useful to add a name
property to each method
, for reflection or some other purpose in the future.
David queried how often such a property would ever be used in practice, and on reflection I myself had not used it.
7. Arguments and the chain function
The chain
function did not allow the calling function to specify any arguments to pass to the overridden method, only the arguments the calling function itself was supplied with.
David pointed out that he had deliberately left this out, but production would need to have it. I think his later version decided to included.
Separately, I wondered if we could avoid passing the this
object as an argument to the chain function, by making chain
a method on the prototype.
David again had started with this, but left it out, as it was complicated to ensure that you did not duplicate the chain
function as a property in each extended class.
My solution is to provide it as a method on a base class that all classes inherit. I use this in a later blog entry on this subject.
8. Generalised Chain Method
I wondered whether it would be useful to design the chain
method to allow you to call an arbitrary method on a super class. I.e. it would not just chain the function in which it is called, but you could supply a different function name and it would call the first method of the supplied name appearing on a higher prototype than the prototype on which the calling method sits.
This is what the super
and base
keywords in Java and C# do (almost).
In the end, I did not raise this point with David, and I make some further comments on this in a separate blog entry.
9. Super Constructor
This function forces you to call the super constructor (extend
) before the init
function.
In my next blog entry I discuss more flexible options for this.
Try it out
Here is the full code I have amended to try out.
function Class(data)
{
// 1 - WILL AVOIDING LOCAL NAMES SAVE MEMORY?
data.extend = data.extend || null;
data.init = data.init || null;
data.name = data.name || "Unnamed Class";
data.statics = data.statics || {};
data.mixins = data.mixins || [];
data.methods = data.methods || {};
// 1 - END
// MOVED THE MIXINS PREPARATION HERE AS USED EARLIER IN THIS VERSION
if (!(data.mixins instanceof Array)) {
data.mixins = [data.mixins];
}
// END
// 2 - APPLY MIXIN CONSTRUCTORS
data.applyMixinsConstructors = !!data.applyMixinsConstructors;
// 2 - END
// 3 - INHERIT STATICS
data.inheritStatics = !!data.inheritStatics;
// 3 - END
// 5 - MIXINS IN PROTOTYPE CHAIN
data.putMixinsInSuperChain = !!data.putMixinsInSuperChain;
data.noMixinIfConflict = !!data.noMixinIfConflict;
// 5 - END
// 1 - NEED SOME CLOSURES FOR CONSTRUCTOR FUNCTION
var extend = data.extend;
var init = data.init;
var mixins = data.mixins
var applyMixinsConstructors = data.applyMixinsConstructors;
// 1 - END
// 0 - DONT ENUM ISSUE
var dontEnum = {"toString" : 1, "valueOf" : 1};
var dontEnumArray = ["toString", "valueOf"];
// 0 - END
var constructor = function() {
// 2 - APPLY MIXINS CONSTRUCTORS
if (mixins && applyMixinsConstructors) {
for (var k in data.mixins) {
data.mixins[k].apply(this);
}
}
// 2 - END
if (extend) {
extend.apply(this, arguments);
}
if (init) {
init.apply(this, arguments);
}
};
for(var p in data.statics) {
constructor[p] = data.statics[p];
}
// 3 - INHERIT STATICS
if (data.inheritStatics) {
if (data.extend) {
inheritStatics(constructor, data.extend);
}
if (data.mixins) {
for (var i = 0; i < data.mixins.length; i++) {
inheritStatics(constructor, data.mixins[i]);
}
}
}
function inheritStatics(constuctor, superclass)
{
while (superclass) {
for (var p in superclass) {
if (typeof superclass[p] == "function") {
constructor[p] = superclass[p];
}
}
}
superclass = superclass.superclass;
}
// 3 - END
constructor.superclass = data.extend || Object;
constructor.classname = data.name;
// 4 - DUMMY CONSTRUCTOR FOR MIXINS
var mixinConstructor = function(){};
var mixinProto = mixinConstructor.prototype = new constructor.superclass();
mixinProto.superProto = constructor.superclass.prototype;
var proto = constructor.prototype = new mixinConstructor();
proto.superProto = mixinProto;
// 4 - END
for(var p in proto) {
if (proto.hasOwnProperty(p)) {
delete proto[p];
}
}
for (var i = 0; i < data.mixins.length; i++) {
var m = data.mixins[i].prototype;
for(var p in m) {
// 0 - DONT ENUM BUG - MAKE OTHER BROWSERS BEHAVE LIKE IE
if (p in dontEnum) {
continue;
}
// 0 - END
//alert("mixin " + p);
// 5 - MIXIN IN SUPER CHAIN
if (typeof m[p] == "function") {
if (data.putMixinsInSuperChain) {
m[p].overrides = mixinProto[p];
} else if (data.noMixinIfConflict && mixinProto[p]) {
continue;
}
mixinProto[p] = m[p];
m[p].ownerProto = mixinProto;
} else if (typeof m[p] != "undefined") {
//FORGOT - SHOULD BE FUNCTIONS ONLY
//mixinProto[p] = m[p];
//m[p].ownerProto = mixinProto;
}
// 5 - END
}
// 0 - DONT ENUM BUG
for (var i in dontEnumArray) {
var p = dontEnumArray[i];
if (!m.hasOwnProperty(p)){
continue;
}
var f = m[p];
if (typeof f == "function") {
if (data.putMixinsInSuperChain) {
f.overrides = mixinProto[p];
} else if (data.noMixinIfConflict && mixinProto[p]) {
continue;
}
mixinProto[p] = f;
f.ownerProto = mixinProto;
}
}
// 0 - END
}
for(var p in data.methods) {
// 0 - DONT ENUM BUG - MAKE OTHER BROWSERS BEHAVE LIKE IE
if (p in dontEnum) {
continue;
}
// 0 - END
//alert("method " + p);
var m = data.methods[p];
if (typeof m == "function") {
m.overrides = mixinProto[p];
proto[p] = m;
// 6 - GIVE FUNCTIONS A NAME?
m.name = p;
// 6 - END
// 8 - REMEMBER WHICH PROTO IT BELONGS TO (SEE BELOW)
m.ownerProto = proto;
// 8 - END
}
}
// 0 - DONT ENUM BUG
for (var i in dontEnumArray) {
var p = dontEnumArray[i];
if (!data.methods.hasOwnProperty(p)){
continue;
}
var m = data.methods[p];
if (typeof m == "function") {
m.overrides = mixinProto[p];
proto[p] = m;
// 6 - GIVE FUNCTIONS A NAME?
m.name = p;
// 6 - END
// 8 - REMEMBER WHICH PROTO IT BELONGS TO (SEE BELOW)
m.ownerProto = proto;
// 8 - END
}
}
// 0 - END
proto.constructor = constructor;
// 7 - CALL SUPER METHOD (INSTEAD OF CHAIN)
if (!proto["callSuperThis"]) {
proto["callSuperThis"] = function(args) {
var m = args.callee;
var om = m.overrides;
if (om) {
// MAKE IT UP TO THE CALLING FUNCTION TO EXPLICITLY SUPPLY ARGUMENTS
var superArgs = Array.prototype.slice.call(arguments, 1);
return om.apply(this, superArgs)
}
};
}
// 7 - END
// 8 - GENERAL CALL SUPER!?
if (!proto["callSuper"]) {
proto["callSuper"] = function(method, args) {
if (typeof method == "string") {
var superProto = args.callee.ownerProto.superProto;
var m = superProto[method];
} else if (typeof method == "function") {
var m = method.override;
} else { return; }
if (m) {
// MAKE IT UP TO THE CALLING FUNCTION TO EXPLICITLY SUPPLY ARGUMENTS
var superArgs = Array.prototype.slice.call(arguments, 2);
return m.apply(this, superArgs)
}
};
}
// 8 - END
// 1 - SAVE MEMORY?
data = null;
// 1 - END
return constructor;
}
var GenericEquals = Class({
name: "GenericEquals",
methods: {
equals: function(that) {
if (this == that) return true;
var propsInThat = 0;
for(var name in that) {
if (!that.hasOwnProperty(name)){ continue; }
propsInThat++;
if (this[name] !== that[name]) return false;
}
// Now make sure that this object doesn't have additional props
var propsInThis = 0;
for(name in this){
if (!this.hasOwnProperty(name)){ continue; }
propsInThis++;
}
// If this has additional properties then they are not equal
if (propsInThis != propsInThat) return false;
// The two objects appear to be equal.
return true;
},
toString : function() {
return "mixin" + this.callSuperThis(arguments);
}
}
});
var Rectangle = Class({
name: "Rectangle",
init: function(w,h) {
this.width = w; this.height = h;
},
methods: {
area: function(scale) { return this.width * this.height * (scale || 1); },
compareTo: function(that) { return this.area() - that.area(); },
toString: function() {
return "[" + this.width + "," + this.height + "]"
}
}
});
var PositionedRectangle = Class({
name: "PositionedRectangle",
extend: Rectangle,
init: function(w,h,x,y) {
this.x = x;
this.y = y;
},
methods: {
isInside: function(x,y) {
return x > this.x && x < this.x+this.width &&
y > this.y && y < this.y+this.height;
},
toString: function() {
return this.callSuperThis(arguments) + "(" + this.x + "," + this.y + ")";
}
},
mixins: [GenericEquals],
putMixinsInSuperChain : true
});
var ColoredRectangle = new Class({
name: "ColoredRectangle",
extend: PositionedRectangle,
init: function(w,h,x,y,c) { this.c = c; },
methods: {
toString: function() { return this.callSuperThis(arguments) + ": " + this.c},
area : function(scale) { return this.callSuper("area", arguments, scale); }
}
});
function test()
{
var cr = new ColoredRectangle(2,2,3,4,5);
var cr2 = new ColoredRectangle(2,2,3,4,5);
alert(cr.toString());
alert(cr.equals(cr2));
alert(cr.area(2));
}
test();
Following feed back on comments I produced the following amended code to reflect my understanding.
function Class(
data /*: Object*/
) /*: void*/
{
var extend /*: Function*/ = data.extend || null;
var init /*: Function*/ = data.init || null;
var name /*: String*/ = data.name || "Unnamed Class";
var statics /*: Object*/ = data.statics || {};
var mixins /*: Object*/ = data.mixins || [];
var methods /*: Object*/ = data.methods || {};
var noMixinIfConflict /*: Boolean*/ = !!data.noMixinIfConflict;
if (!(mixins instanceof Array)) {
mixins = [mixins];
}
var dontEnum /*: Object*/ = {"toString" : 1, "valueOf" : 1};
var dontEnumArray /*: Array.<String>*/ = ["toString", "valueOf"];
var constructor /*: Function*/ = function() {
if (extend) {
extend.apply(this, arguments);
}
if (init) {
init.apply(this, arguments);
}
};
for(var p /*: String*/ in statics) {
constructor[p] = statics[p];
}
constructor.superclass = extend || Object;
constructor.classname = name;
var proto /*: Object*/ = constructor.prototype = new constructor.superclass();
for(var p /*: String*/ in proto) {
if (proto.hasOwnProperty(p)) {
delete proto[p];
}
}
proto.superProto /*: Object*/ = constructor.superclass.prototype;
for (var i /*: int*/ = 0; i < mixins.length; i++) {
var m /*: Object*/ = mixins[i].prototype;
for(var p /*: String*/ in m) {
if (p in dontEnum) {
continue;
}
if (noMixinIfConflict && proto[p]) {
continue;
}
if (typeof m[p] == "function") {
proto[p] = m[p];
}
}
for (var i /*: String*/ in dontEnumArray) {
var p /*: Object*/ = dontEnumArray[i];
if (!m.hasOwnProperty(p)){
continue;
}
var f = m[p];
if (typeof f == "function") {
proto[p] = f;
}
}
}
for(var p /*: String*/ in methods) {
if (p in dontEnum) {
continue;
}
var m /*: Function*/ = methods[p];
if (typeof m == "function") {
m.overrides = proto[p];
proto[p] = m;
m.name = p;
m.ownerProto = proto;
}
}
for (var i /*: int*/ in dontEnumArray) {
var p /*: String */ = dontEnumArray[i];
if (!methods.hasOwnProperty(p)){
continue;
}
var m /*: Function */ = methods[p];
if (typeof m == "function") {
m.overrides = proto[p];
proto[p] = m;
m.name = p;
m.ownerProto = proto;
}
}
proto.constructor /*: Function */ = constructor;
if (!proto["callSuperThis"]) {
proto["callSuperThis"] = function(args /*: {Function Arguments Object}*/) {
var m /*: Function*/ = args.callee;
var om /*: Function*/ = m.overrides;
if (om) {
var superArgs /*: Array*/ = Array.prototype.slice.call(arguments, 1);
return om.apply(this, superArgs);
}
};
}
if (!proto["callSuper"]) {
proto["callSuper"] = function(method, args) {
if (typeof method == "string") {
var superProto = args.callee.ownerProto.superProto;
var m /*: String*/ = superProto[method];
} else if (typeof method == "function") {
var m /*: Function*/ = method.override;
} else { return; }
if (m) {
var superArgs /*: Array*/ = Array.prototype.slice.call(arguments, 2);
return m.apply(this, superArgs);
}
};
}
statics = null;
mixins = null;
methods = null;
data = null;
return constructor;
}
var GenericEquals = Class({
name: "GenericEquals",
methods: {
equals: function(that) {
if (this == that) return true;
var propsInThat = 0;
for(var name in that) {
if (!that.hasOwnProperty(name)){ continue; }
propsInThat++;
if (this[name] !== that[name]) return false;
}
// Now make sure that this object doesn't have additional props
var propsInThis = 0;
for(name in this){
if (!this.hasOwnProperty(name)){ continue; }
propsInThis++;
}
// If this has additional properties then they are not equal
if (propsInThis != propsInThat) return false;
// The two objects appear to be equal.
return true;
}
}
});
var Rectangle = Class({
name: "Rectangle",
init: function(w,h) {
this.width = w; this.height = h;
},
methods: {
area: function(scale) { return this.width * this.height * (scale || 1); },
compareTo: function(that) { return this.area() - that.area(); },
toString: function() {
return "[" + this.width + "," + this.height + "]"
}
}
});
var PositionedRectangle = Class({
name: "PositionedRectangle",
extend: Rectangle,
init: function(w,h,x,y) {
this.x = x;
this.y = y;
},
methods: {
isInside: function(x,y) {
return x > this.x && x < this.x+this.width &&
y > this.y && y < this.y+this.height;
},
toString: function() {
return this.callSuperThis(arguments) + "(" + this.x + "," + this.y + ")";
}
},
mixins: [GenericEquals]
});
var ColoredRectangle = new Class({
name: "ColoredRectangle",
extend: PositionedRectangle,
init: function(w,h,x,y,c) { this.c = c; },
methods: {
toString: function() { return this.callSuperThis(arguments) + ": " + this.c},
area : function(scale) { return this.callSuper("area", arguments, scale + 1); }
}
});
function test()
{
var cr = new ColoredRectangle(2,2,3,4,5);
var cr2 = new ColoredRectangle(2,2,3,4,5);
alert(cr.toString());
alert(cr.equals(cr2));
alert(cr.area(2));
}
test();
Conclusion
I appreciate that this is a good and popular way to emulate clases, but I do worry a little about this kind of style.
If ECMA Script 4 comes along, and you want to use ECMA Script 4's classes, this style might be a bit fiddly to convert to the traditional class style.
David's Follow Up
David posted a follow up here
His final code is repoduced here:-
/**
* defineClass() -- a utility function for defining JavaScript classes.
*
* This function expects a single object as its only argument. It defines
* a new JavaScript class based on the data in that object and returns the
* constructor function of the new class.
*
* The object passed as an argument should have some or all of the
* following properties:
*
* name: the name of the class being defined.
* If specified, this value will be stored in the classname
* property of the returned constructor object.
*
* extend: The constructor of the class to be extended. The returned
* constructor automatically chains to this function. This value
* is stored in the superclass property of the constructor object.
*
* init: The initialization function for the class. If defined, the
* constructor will pass all of its arguments to this function.
* The constructor also automatically invokes the superclass
* constructor with the same arguments, so this function must expect
* the same arguments, in the same order, as the superclass
* constructor, and can add additional arguments at the end.
*
* methods: An object that specifies the instance methods (and other
* non-method properties for the class. The properties of
* this object become properties of the prototype. Methods
* are given an overrides property for chaining. They can
* call "chain(this, arguments)" to invoke the method they
* override. This function adds properties to the methods in
* this object, so you may not pass the same method in two
* invocations of defineClass().
*
* statics: An object that specifies the static methods (and other static
* properties) for the class. The properties of this object become
* properties of the constructor function.
**/
function defineClass(data) {
// Extract some properties from the argument object
var extend = data.extend;
var superclass = extend || Object;
var init = data.init;
var classname = data.name || "Unnamed class";
var methods = data.methods || {};
var statics = data.statics || {};
// Make a constructor function that chains to the superclass constructor
// and then calls the initialization method of this class.
// This will become the return value of this defineClass() method.
var constructor = function() {
if (extend) extend.apply(this, arguments); // Initialize superclass
if (init) init.apply(this, arguments); // Initialize ourself
};
// Copy static properties to the constructor function
if (data.statics)
for(var p in data.statics) constructor[p] = data.statics[p];
// Set superclass and classname properties of the constructor
constructor.superclass = superclass;
constructor.classname = classname;
// Create the object that will be the prototype for the class.
// This new object must inherit from the superclass prototype.
var proto = (superclass == Object) ? {} : heir(superclass.prototype);
// Copy instance methods (and other properties) to the prototype object.
for(var p in methods) { // For each name in methods object
if (p == "toString") continue; // Handled below
var m = methods[p]; // This is the value to copy
if (typeof m == "function") { // If it is a function
m.overrides = proto[p]; // Remember anything it overrides
m.name = p; // Tell it what its name is
m.owner = constructor; // Tell it what class owns it.
}
proto[p] = m; // Then store in the prototype
}
// In IE, a for/in loop won't enumerate properties that have the same name
// as non-enumerable Object methods like toString(). As a partial
// work-around, we handle the toString method specially
if (methods.hasOwnProperty("toString")) { // IE DontEnum bug
methods.toString.overrides = proto.toString;
methods.toString.name = "toString";
methods.toString.owner = constructor;
proto.toString = methods.toString;
}
// All objects should know who their constructor was
proto.constructor = constructor;
// And the constructor must know what its prototype is
constructor.prototype = proto;
// Finally, return the constructor function
return constructor;
}
/**
* Return a new object with p as its prototype
*/
function heir(p) {
function h(){}
h.prototype=p;
return new h();
}
/**
* Chain from the calling function to the function on its overrides property.
* Invoke that method on the first argument. The second argument must be the
* arguments object of the calling function: its callee property is used to
* determine what function is doing the chaining. The third argument is an
* optional array of values to pass to the overridden method. If omitted,
* the second argument is used instead, passing all of the caller's arguments
* on to the overridden method.
*
* This method returns the return value of the overridden method or
* throws "ChainError" if no overridden method could be found
*
* Typical invocation: chain(this, arguments)
* To pass different args: chain(this, arguments, [w, h])
*/
function chain(o, args, pass) {
var f = args.callee; // The calling function.
var g = f.overrides; // The function it chains to.
var a = pass || args; // The arguments we'll pass to s
if (g) return g.apply(o, a); // Call o.g(a) and return its value as ours.
else throw "ChainError" // Complain if nothing to override
}
No comments made on this entry.