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

Post a comment

Options:

Size

Colors