yet another javascript inheritance implementation for prototype.js
introduction
As I was writing a complex web application for my school, I feel like I was terribly in need of a nice and useful inheritance mechanism for my javascript code.
I went on the Ruby On Rails Trac and started to look after some interesting patch.
What I found was the famous ticket 4060 submitted by Ben Newman and a link to the paper he wrote about this.
This was really interesting but unfortunately, the source code was too big to be integrated in Prototype. Then I found Base.js on Dean Edward's blog which I liked a lot, but wasn't really the Prototype way (especially the extend semantic).
Another interesting candidate was inheritance.js posted at twologic but I realized it wasn't using a real prototype inheritance (note the new version does, and has inspired Prototype 1.6) but modifying functions (if you decide to redefine a method at runtime, it won't be able to call the parent one).
So I decided to look around and do something by myself with the minimum code possible and looking like Ruby syntax.
usage
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
var Person = Class.create({ // class declaration self: { // class method find: function(name) { return this.population.find(function(p) { return p.name == name; }); }, // class attribute population: [] } // instance declaration // mixins include: [ Comparable, Loggable ], // imaginary mixins initialize: function(name) { this.name = name; // accessing class from instance this.constructor.population.push(this); }, introduce: function() { return My name is + this.name; } }); // We extend Person class var Employee = Class.create(Person, { initialize: function(name, salary) { // call the parent initialize method this.callSuper('initialize', name); this.salary = salary; }, introduce: function() { return this.callSuper('introduce') + and I earn + this.salary; } }); var jack = new Person('Jack'); var billy = new Employee('Billy', 2000); jack.introduce(); // -> My name is Jack billy.introduce(); // -> My name is Billy and I earn 2000 Person.find('Jack').name; // -> 'Jack' Person.find('Billy'); // -> null Employee.find('Billy').name; // -> 'Billy' |
important points
- Class.create is 100% compatible with Prototype and gives an empy initialize method in case no one is given
- Object.prototype isn't modified
- Class declaration goes in self
- initialize special class method is executed at class creation
- In a subclass, callSuper is available as a class method or an instance method, and calls respectively class or instance method with the given name and arguments in superclass
- When deriving a class, we copy it's method and attributes in the subclass, and we inherit it's prototype (modified class method won't be modified in subclass, except if it calls callSuper)
- Every class has 2 two class methods : include and extend, they simply act like their ruby equivalents (see further).
- A subclass has a superclass attribute
- Any initialize method can be omitted, default behavior is calling the parent one
let's go further
What about private methods and attributes ? Well, to be prototype compatible, it seems that we need a dirty hook in class constructor (which is not the initialize function but the one returned by Class.create) so maybe we should just use some convention, like this._one, and be conscious of what we're doing (If I'm calling a method which starts with an underscore from something that is not this, I'm certainly doing something wrong...).
How can we include a mixin after declaration ?The good old Prototype way still works :
Object.extend(MyClass.prototype, MyMixin)
The new funky way :
MyClass.include(MyMixin);
While
MyClass.extend(MyMixin)
litterally extends class with mixin, making MyMixin methods and attributes available in MyClass
Be careful, mixins here are not ruby modules ! They are not part of the inheritance hierarchy. If they redefine a method, the previous one won't be available by calling callSuper.
source
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 |
/* Copyright (c) 2007 Samuel Lebeau Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the Software), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ var Class = { create: function(superclass, body) { if (!body) { var body = superclass || {}; var superclass = null; } var klass = function() { this.constructor = klass; if (!Class._prototyping_) this.initialize.apply(this, arguments); } if (superclass) { Object.extend(klass, superclass); Object.extend(klass, { superclass: superclass, callSuper: Class._bindCallSuper(superclass), prototype: Class._inheritPrototype(superclass.prototype) }); klass.prototype.callSuper = Class._bindCallSuper(superclass.prototype) } klass.include = Class._include; klass.extend = Class._extend; if (body.self) { Object.extend(klass, body.self); delete body.self; } if (body.include) { [ body.include ].flatten().each(function(mixin) { klass.include(mixin); }); delete body.include; } Object.extend(klass.prototype, body); if (!klass.prototype.initialize) klass.prototype.initialize = Prototype.emptyFunction; if (klass.initialize) klass.initialize(); return klass; }, _inheritPrototype: function(proto) { var inheritance = function() {}; inheritance.prototype = proto; Class._prototyping_ = true; var prototype = new inheritance(); delete Class._prototyping_; return prototype; }, _include: function(mixin) { Object.extend(this.prototype, mixin); }, _extend: function(mixin) { Object.extend(this, mixin); }, _bindCallSuper: function(ancestor) { return function() { var args = $A(arguments), method = args.shift(), ret; this.callSuper = ancestor.callSuper; try { if (ancestor[method] && ancestor[method] != this[method]) ret = ancestor[method].apply(this, args); else if (!this.callSuper) throw new Class._noSuperMethodError(method); else ret = this.callSuper.apply(this, arguments); } finally { this.callSuper = arguments.callee } if (ret) return ret; } }, _noSuperMethodError: function(method) { this.name = 'NoSuperMethodError'; this.message = 'no super method ' + method + ''; } } |
download
- patch for prototype trunk including tests
- prepatched prototype.js (revision 6729)
installation from trunk
$ cd /path/to/prototype/trunk
$ wget http://svn.gotfresh.info/classjs/class-patch-trunk.diff
$ patch -p0 < class-patch-trunk.diff
$ rake dist
then check test/unit/class.html or 'rake test' !
Comments
-
Looks very Prototypish! Nice
-
This is absolutely the best implementation of Prototype-centric inheritance I've seen. Also one of the most readable. It gives me all the features I'm looking for — in particular inheritable class methods and properties. I have however noticed one caveat. If you try to create a subclass without passing in a body, the sub class doesn't actually inherit any of the super's methods or properties. Not a bug as such, but it does make it difficult to create a subclass and then extend it later. I've actually created a fix for this. All it does it check to see if superclass is an 'object' or 'function'. If it's a function then we know it's a superclass to inherit from, otherwise it's a hash which we're creating a new class with. If you'd like this as a patch, send me an email and I'd be happy to diff it and send it to you. Also, can you tell me what the license on this code is? I'm interested in using it in a commercial project and what to make sure I'm not doing that against your intentions. Thanks again!