yet another javascript inheritance implementation for prototype.js

written by sam on April 12th, 2007 @ 07:00 AM

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

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

  • Mislav on 14 Apr 18:51

    Looks very Prototypish! Nice
  • Mr eel on 09 May 06:16

    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!

Post a comment

Options:

Size

Colors