Javascript OO Ruby Style
This is a follow up post regarding the OO inheritance javascript library I blogged about earlier. I planned on submitting my original work as a patch to Prototype. Before submitting my patch at the rails development site I checked out the competition. I found a couple OO implementations already in contention for the next major version of Prototype. For the curious, both of them are mentioned on Sam Stephenson's weblog.
I spent some time looking over both solutions. Each one tried and did a very good job of implementing OO features, but they both had parts I didn't like. One author thought it would be neat to add support for private methods. This is neat, but I don't think javascript really needs this. People have been writing javascript for years without it and every attempt I've seen to add it to the language has seemed kind of hackish, well at least to me. You'll find my biggest beef with both libraries when you inspect an instance of one of your classes. You'll find a couple added methods and properties that the library injects so that it can do it's job. In fact even my previous OO library was guilty of this, something that bugged me to no end.
So I sat down one evening and rewrote the core of my own library to meet all my demands and expectations. I am proud to say that the new library is much smaller, sleeker and generally just feels better than the first. I also added some neat new features along the way.
Visit the project page for details on how to get the library.
Just like Dean Edwards, I want an OO library that solves a number of problems.
For convenience, here is Dean's list.
http://dean.edwards.name/weblog/2006/03/
- I want to easily create classes without theMyClass.prototype cruft
- I want method overriding with intuitive access to the overridden method (like Java’s super)
- I want to avoid calling a class’ constructor function during the prototyping phase
- I want to easily add static (class) properties and methods
- I want to achieve the above without resorting to global functions to build prototype chains
- I want to achieve the above without affectingObject.prototype
However, I have a few additions.
- I want a more Ruby like feel to creating and working with Objects.
- I want to achieve all of the above without adding additional baggage to final object.
- I want to achieve all of the above while maintaining backward compatibility with Prototype.
So what's so new and great with my OO library? Well, since creating the first version I've learned a few neat tricks to help achieve my goals. First lets talk about mixins. Previously you had to define your mixins on a separate line
after your class definition.
Debug = {
log: function(msg) {
alert(msg);
}
};
Employee = Class.create({
initialize: function() {
this.log("In the Employee constructor!");
}
});
Class.mixin(Employee.prototype, Debug);
Client = Class.create({
initialize: function() {
this.log("In the Client constructor!");
}
});
Class.mixin(Client.prototype, Debug);
Well I've added a bit of syntax sugar that in turn adds more Ruby flavor. It’s now possible to statically define an objects mixins within the class definition.
Comparable = {
// define methods...
};
Debug = {
// define methods...
};
Employee = Class.create({
// define a single mixin
include: Debug,
initialize: function() {
// ...
}
});
Client = Class.create({
// define an array of mixins
include: [Debug, Comparable],
initialize: function() {
// ...
}
});
Each element given to the include property will have all of it's methods mixed into the objects' s definition. All standard rules for Ruby mixins are followed and apply when added this way. Also, it's worth noting that the include
property will be removed from the object's definition. This way you won't see it when you inspect instances of your class.
The second improvement is in the this.parent()
method. It's now smart enough to recognize what method it is being used inside of, so you no longer have to explicitly tell it what method to call.
Manager = Class.create(Employee, {
initialize: function(name, dept, title) {
this.parent(name, dept);
this.title = title;
},
doWork: function() {
this.parent();
}
});
If you have defined a property or method using the same name, it will overwritten. It's also important to note that the 'this.parent()' method is only available inside a method definition. Doing something like the following will result in a javascript error.
var joe = new Manager();
joe.parent();
The last visible difference between this version and my previous is that I no longer force properties into the generated objects. This is best demonstrated with an example.
Debug = {
log: function(msg) {
document.writeln(msg);
}
}
Employee = Class.create({
initialize: function(name, dept) {
<a href="http://this.name">this.name</a> = name;
this.dept = dept;
},
doWork: function() {
return <a href="http://this.name">this.name</a> + " is now working...";
}
});
Manager = Class.create(Employee, {
include: Debug,
initialize: function(name, dept, title) {
this.parent(name, dept);
this.title = title;
},
doWork: function() {
this.log(this.parent() + "at managing the employees.");
}
});
var joe = new Manager("Joe", "Sales", "Sales Manager");
joe.doWork()
var out = "";
for (var p in joe) {
out += p + ": " + joe[p] + ",\\n"
}
out = out.substring(0, out.length - 2);
Debug.log("Manager {\\n" + out + "\\n}");
Outputs:
Joe is now working...at managing the employees.
Manager {
name: Joe,
dept: Sales,
title: Sales Manager,
initialize: function (name, dept, title) {
this.parent(name, dept);
this.title = title;
},
doWork: function () {
this.log(this.parent() + "at managing the employees.");
},
log: function (msg) {
document.writeln(msg);
}
}
As you can see the for (var p in joe)
construct returns exactly what you defined. There are no extra methods or properties visibly added to you object to make any of this work. You can also see that the log method was mixed into the class definition and the include property no longer exists.
Visit the project page for details on how to get the library.