views:

292

answers:

3

John Resig (of jQuery fame) provides a concise and elegant way to allow simple JavaScript inheritance.

It was so short and sweet, in fact, that it inspired me to attempt to simplify and improve it even further. I've modified Resig's original Class.extend function such that it passes all of his inheritance tests (plus a few more), and also has the following advantages:

  • simplicity (less code and you don't have to be a ninja to understand it)
  • performance (no extra overhead for base/super method calls)
  • consistency (familiar C# syntax for base/super method calls)
  • tool friendly (constructor syntax no longer crashes Visual Studio and provides complete "IntelliSense" support)

Because it almost seems too good to be true, I want to ensure my logic doesn't have any fundamental flaws or bugs, and see if anyone can suggest improvements or refute the code entirely.

Does anyone see anything wrong with my approach (below) vs. John Resig's original approach?

if (!window.My)
    window.My = {};

My.Object = function()
{
    /// <summary>Initializes a new instance of the My.Object class.</summary>
};

My.Object.isReservedKeyword = function(value)
{
    /// <summary>Checks a string value against the list of reserved keywords.</summary>
    /// <param name="value">The value to check.</param>
    /// <returns>True if the value is a reserved keyword.</returns>

    return /^(base|constructor|ctor)$/.test(value);
};

My.Object.extend = function(prototype)
{
    /// <summary>Creates a new derived class by extending the calling base class.</summary>
    /// <param name="prototype">The prototype constructor and members of the derived class.</param>
    /// <returns>The new derived class.</returns>

    // create the base class
    var Base = function() {};
    Base.prototype = this.prototype;

    // create the derived class
    var Derived = (prototype && prototype.ctor) ? prototype.ctor : function() {};
    Derived.prototype = new Base;
    Derived.prototype.base = Base.prototype;
    Derived.prototype.constructor = Derived;

    // extend the base prototype
    for (var i in prototype)
        if (!My.Object.isReservedKeyword(i))
            Derived.prototype[i] = prototype[i];

    // make the derived class extendable
    Derived.extend = arguments.callee;

    // return the derived class
    return Derived;
};

And the usage (below) is nearly identical to John Resig's original usage except for the syntax around base method calls and the way the constructor is specified (for the reasons enumerated above). I've also included a few more usage examples and tests.

var Minimal = My.Object.extend();

var Simple = My.Object.extend(
{
    exists: function()
    {
        return true;
    }
});

var Person = My.Object.extend(
{
    ctor: function(isDancing)
    {
        this.dancing = isDancing;
    },
    dance: function()
    {
        return this.dancing;
    }
});

var Ninja = Person.extend(
{
    ctor: function()
    {
        this.base.constructor.call(this, false);
        this.attacking = true;
    },
    dance: function()
    {
        return this.base.dance.call(this);
    },
    attack: function()
    {
        return this.attacking;
    }
});

var m = new Minimal();
var s = new Simple();
var p = new Person(true);
var n = new Ninja();

// Test 1
if (!(m.constructor === Minimal && m instanceof Minimal && m instanceof My.Object))
    alert("Test 1 failed.");

// Test 2
if (!(s.constructor === Simple && s instanceof Simple && s instanceof My.Object))
    alert("Test 2 failed.");

// Test 3
if (!s.exists())
    alert("Test 3 failed.");

// Test 4
if (!p.dance())
    alert("Test 4 failed.");

// Test 5
if (!(p.constructor === Person && p instanceof Person && p instanceof My.Object))
    alert("Test 5 failed.");

// Test 6
if (n.dance())
    alert("Test 6 failed.");

// Test 7
n.dancing = true;
if (!n.dance())
    alert("Test 7 failed.");

// Test 8
if (!n.attack())
    alert("Test 8 failed.");

// Test 9
if (!(n.constructor === Ninja && n instanceof Ninja && n instanceof Person && n instanceof My.Object))
    alert("Test 9 failed.");

NOTE: The above code has been updated several times since I initially posted this question. The above represents the latest version. To see how it has evolved, please check the revision history.

+2  A: 

Not so fast. It just doesn't work.

Consider:

var p = new Person(true);
alert("p.dance()? " + p.dance()); => true

var n = new Ninja();
alert("n.dance()? " + n.dance()); => false
n.dancing = true;
alert("n.dance()? " + n.dance()); => false

base is just another object initialized with default members that made you think it works.

EDIT: for the record, here is my own (albeit more verbose) implementation of Java like inheritance in Javascript, crafted in 2006 at the time I got inspired by Dean Edward's Base.js (and I agree with him when he says John's version is just a rewrite of his Base.js). You can see it in action (and step debug it in Firebug) here.

/**
 * A function that does nothing: to be used when resetting callback handlers.
 * @final
 */
EMPTY_FUNCTION = function()
{
  // does nothing.
}

var Class =
{
  /**
   * Defines a new class from the specified instance prototype and class
   * prototype.
   *
   * @param {Object} instancePrototype the object literal used to define the
   * member variables and member functions of the instances of the class
   * being defined.
   * @param {Object} classPrototype the object literal used to define the
   * static member variables and member functions of the class being
   * defined.
   *
   * @return {Function} the newly defined class.
   */
  define: function(instancePrototype, classPrototype)
  {
    /* This is the constructor function for the class being defined */
    var base = function()
    {
      if (!this.__prototype_chaining 
          && base.prototype.initialize instanceof Function)
        base.prototype.initialize.apply(this, arguments);
    }

    base.prototype = instancePrototype || {};

    if (!base.prototype.initialize)
      base.prototype.initialize = EMPTY_FUNCTION;

    for (var property in classPrototype)
    {
      if (property == 'initialize')
        continue;

      base[property] = classPrototype[property];
    }

    if (classPrototype && (classPrototype.initialize instanceof Function))
      classPrototype.initialize.apply(base);

    function augment(method, derivedPrototype, basePrototype)
    {
      if (  (method == 'initialize')
          &&(basePrototype[method].length == 0))
      {
        return function()
        {
          basePrototype[method].apply(this);
          derivedPrototype[method].apply(this, arguments);
        }
      }

      return function()
      {
        this.base = function()
                    {
                      return basePrototype[method].apply(this, arguments);
                    };

        return derivedPrototype[method].apply(this, arguments);
        delete this.base;
      }
    }

    /**
     * Provides the definition of a new class that extends the specified
     * <code>parent</code> class.
     *
     * @param {Function} parent the class to be extended.
     * @param {Object} instancePrototype the object literal used to define
     * the member variables and member functions of the instances of the
     * class being defined.
     * @param {Object} classPrototype the object literal used to define the
     * static member variables and member functions of the class being
     * defined.
     *
     * @return {Function} the newly defined class.
     */
    function extend(parent, instancePrototype, classPrototype)
    {
      var derived = function()
      {
        if (!this.__prototype_chaining
            && derived.prototype.initialize instanceof Function)
          derived.prototype.initialize.apply(this, arguments);
      }

      parent.prototype.__prototype_chaining = true;

      derived.prototype = new parent();

      delete parent.prototype.__prototype_chaining;

      for (var property in instancePrototype)
      {
        if (  (instancePrototype[property] instanceof Function)
            &&(parent.prototype[property] instanceof Function))
        {
            derived.prototype[property] = augment(property, instancePrototype, parent.prototype);
        }
        else
          derived.prototype[property] = instancePrototype[property];
      }

      derived.extend =  function(instancePrototype, classPrototype)
                        {
                          return extend(derived, instancePrototype, classPrototype);
                        }

      for (var property in classPrototype)
      {
        if (property == 'initialize')
          continue;

        derived[property] = classPrototype[property];
      }

      if (classPrototype && (classPrototype.initialize instanceof Function))
        classPrototype.initialize.apply(derived);

      return derived;
    }

    base.extend = function(instancePrototype, classPrototype)
                  {
                    return extend(base, instancePrototype, classPrototype);
                  }
    return base;
  }
}

And this is how you use it:

var Base = Class.define(
{
  initialize: function(value) // Java constructor equivalent
  {
    this.property = value;
  }, 

  property: undefined, // member variable

  getProperty: function() // member variable accessor
  {
    return this.property;
  }, 

  foo: function()
  {
    alert('inside Base.foo');
    // do something
  }, 

  bar: function()
  {
    alert('inside Base.bar');
    // do something else
  }
}, 
{
  initialize: function() // Java static initializer equivalent
  {
    this.property = 'Base';
  },

  property: undefined, // static member variables can have the same
                                 // name as non static member variables

  getProperty: function() // static member functions can have the same
  {                                 // name as non static member functions
    return this.property;
  }
});

var Derived = Base.extend(
{
  initialize: function()
  {
    this.base('derived'); // chain with parent class's constructor
  }, 

  property: undefined, 

  getProperty: function()
  {
    return this.property;
  }, 

  foo: function() // override foo
  {
    alert('inside Derived.foo');
    this.base(); // call parent class implementation of foo
    // do some more treatments
  }
}, 
{
  initialize: function()
  {
    this.property = 'Derived';
  }, 

  property: undefined, 

  getProperty: function()
  {
    return this.property;
  }
});

var b = new Base('base');
alert('b instanceof Base returned: ' + (b instanceof Base));
alert('b.getProperty() returned: ' + b.getProperty());
alert('Base.getProperty() returned: ' + Base.getProperty());

b.foo();
b.bar();

var d = new Derived('derived');
alert('d instanceof Base returned: ' + (d instanceof Base));
alert('d instanceof Derived returned: ' + (d instanceof Derived));
alert('d.getProperty() returned: ' + d.getProperty());  
alert('Derived.getProperty() returned: ' + Derived.getProperty());

d.foo();
d.bar();
Gregory Pakosz
Darn, thanks for pointing that major flaw out. I'll take another stab at it, but I'll probably end right up at John Resig's original function.
Will
Sure John's function is perfectly fine (again he should have credited Dean Edwards though). Anyway, go head, take another stab at it like I did back then: it's part of the fun and understanding these inner workings of the language will make you (feel) a better programmer. Interestingly, I never really used my implementation, it was just for the sake of it :) Also I don't really see the point of trying to shrink the maximum amount of logic into the minimal amount of code: sure my version is verbose, but any time I get back reading it I understand what's going on.
Gregory Pakosz
I believe it all works now. I've made a minor change to the base method calls to use the "base.method.call(this)" syntax which fixes the issue you reported. Do you see any other problems with the implementation?I'm not sure this is a pointless exercise. i believe one of the reasons most developers shy away from JavaScript inheritance is because of the "black magic" that's involved with understanding the implementation, or the ugly inheritance syntax they are forced into.I believe this helps to address both concerns (provided it is correct of course).
Will
Understanding the internals isn't pointless. However I've always believed there is little point in the size competition between jQuery, Mootools, etc; but again I never really faced page load slowdowns in my pet projects that were caused by bloated scripts. Then I'm not expert enough (though I believe I did a good homework at implementing Java like inheritance) in Javascript to decide this is the way to go: experts like Douglas Crockford state that one should strive to "fully embrace prototypalism", and "liberate themselves from the confines of the classical model"
Gregory Pakosz
A: 

This is about as simple as you can get. It was taken from http://www.sitepoint.com/blogs/2006/01/17/javascript-inheritance/#.

// copyPrototype is used to do a form of inheritance.  See http://www.sitepoint.com/blogs/2006/01/17/javascript-inheritance/#
// Example:
//    function Bug() { this.legs = 6; }
//    Insect.prototype.getInfo = function() { return "a general insect"; }
//    Insect.prototype.report = function() { return "I have " + this.legs + " legs"; }
//    function Millipede() { this.legs = "a lot of"; }
//    copyPrototype(Millipede, Bug);  /* Copy the prototype functions from Bug into Millipede */
//    Millipede.prototype.getInfo = function() { return "please don't confuse me with a centipede"; } /* ''Override" getInfo() */
function copyPrototype(descendant, parent) {
  var sConstructor = parent.toString();
  var aMatch = sConstructor.match(/\s*function (.*)\(/);
  if (aMatch != null) { descendant.prototype[aMatch[1]] = parent; }
  for (var m in parent.prototype) {

    descendant.prototype[m] = parent.prototype[m];
  }
};
John Fisher
It's simple all right, but not as useful (no base/super access), nor pretty IMO.
Will
@Will: You can access the parent methods. Check the link for more explanations.
John Fisher
+1  A: 

Some time ago, I looked at several object systems for JS and even implemented a few of my own, eg class.js and proto.js.

The reason why I never used them: you'll end up writing the same amount of code. Case in point: Resig's Ninja-example (only added some whitespace):

var Person = Class.extend({
    init: function(isDancing) {
        this.dancing = isDancing;
    },

    dance: function() {
        return this.dancing;
    }
});

var Ninja = Person.extend({
    init: function() {
        this._super(false);
    },

    swingSword: function() {
        return true;
    }
});

19 lines, 264 bytes.

Standard JS with Object.create() (which is an ECMAScript 5 function, but for our purposes can be replaced by a custom ES3 clone() implementation):

function Person(isDancing) {
    this.dancing = isDancing;
}

Person.prototype.dance = function() {
    return this.dancing;
};

function Ninja() {
    Person.call(this, false);
}

Ninja.prototype = Object.create(Person.prototype);

Ninja.prototype.swingSword = function() {
    return true;
};

17 lines, 282 bytes. Imo, the extra bytes are not really woth the added complexity of a seperate object system. It's easy enough to make the standard example shorter by adding some custom functions, but again: it's not realy worth it.

Christoph
+1, very interesting point of view
Alsciende
I used to think the same as you, but now I have to disagree. Basically, all John Resig and I have done is to create a single method ("extend") which wires up the exact object/prototype behavior that you have in your above example (i.e. it's not a new object system). The only difference is the syntax is shorter, tighter, and less error prone when using the extend method.
Will
After reflecting on it, I agree that using the standard wire-up syntax is just as short as using the extend syntax. I do still think the latter is much cleaner, less error prone, and is more of a "convention over configuration" approach, if that makes sense.
Will