Improving Simple JavaScript Inheritance
Asked Answered
C

3

15

John Resig (of jQuery fame) provides a concise implementation of Simple JavaScript Inheritance. His approach inspired my attempt to improve things even further. I've rewritten Resig's original Class.extend function to include the following advantages:

  • Performance – less overhead during class definition, object construction, and base class method calls

  • Flexibility – optimized for newer ECMAScript 5-compatible browsers (e.g. Chrome), but provides equivalent "shim" for older browsers (e.g. IE6)

  • Compatibility – validates in strict mode and provides better tool compatibility (e.g. VSDoc/JSDoc comments, Visual Studio IntelliSense, etc.)

  • Simplicity – you don't have to be a "ninja" to understand the source code (and it's even simpler if you lose the ECMAScript 5 features)

  • Robustness – passes more "corner case" unit tests (e.g. overriding toString in IE)

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. With that, I present the classify function:

function classify(base, properties)
{
    /// <summary>Creates a type (i.e. class) that supports prototype-chaining (i.e. inheritance).</summary>
    /// <param name="base" type="Function" optional="true">The base class to extend.</param>
    /// <param name="properties" type="Object" optional="true">The properties of the class, including its constructor and members.</param>
    /// <returns type="Function">The class.</returns>

    // quick-and-dirty method overloading
    properties = (typeof(base) === "object") ? base : properties || {};
    base = (typeof(base) === "function") ? base : Object;

    var basePrototype = base.prototype;
    var derivedPrototype;

    if (Object.create)
    {
        // allow newer browsers to leverage ECMAScript 5 features
        var propertyNames = Object.getOwnPropertyNames(properties);
        var propertyDescriptors = {};

        for (var i = 0, p; p = propertyNames[i]; i++)
            propertyDescriptors[p] = Object.getOwnPropertyDescriptor(properties, p);

        derivedPrototype = Object.create(basePrototype, propertyDescriptors);
    }
    else
    {
        // provide "shim" for older browsers
        var baseType = function() {};
        baseType.prototype = basePrototype;
        derivedPrototype = new baseType;

        // add enumerable properties
        for (var p in properties)
            if (properties.hasOwnProperty(p))
                derivedPrototype[p] = properties[p];

        // add non-enumerable properties (see https://developer.mozilla.org/en/ECMAScript_DontEnum_attribute)
        if (!{ constructor: true }.propertyIsEnumerable("constructor"))
            for (var i = 0, a = [ "constructor", "hasOwnProperty", "isPrototypeOf", "propertyIsEnumerable", "toLocaleString", "toString", "valueOf" ], p; p = a[i]; i++)
                if (properties.hasOwnProperty(p))
                    derivedPrototype[p] = properties[p];
    }

    // build the class
    var derived = properties.hasOwnProperty("constructor") ? properties.constructor : function() { base.apply(this, arguments); };
    derived.prototype = derivedPrototype;
    derived.prototype.constructor = derived;
    derived.prototype.base = derived.base = basePrototype;

    return derived;
}

And the usage is nearly identical to Resig's except for the constructor name (constructor vs. init) and the syntax for base class method calls.

/* Example 1: Define a minimal class */
var Minimal = classify();

/* Example 2a: Define a "plain old" class (without using the classify function) */
var Class = function()
{
    this.name = "John";
};

Class.prototype.count = function()
{
    return this.name + ": One. Two. Three.";
};

/* Example 2b: Define a derived class that extends a "plain old" base class */
var SpanishClass = classify(Class,
{
    constructor: function()
    {
        this.name = "Juan";
    },
    count: function()
    {
        return this.name + ": Uno. Dos. Tres.";
    }
});

/* Example 3: Define a Person class that extends Object by default */
var Person = classify(
{
    constructor: function(name, isQuiet)
    {
        this.name = name;
        this.isQuiet = isQuiet;
    },
    canSing: function()
    {
        return !this.isQuiet;
    },
    sing: function()
    {
        return this.canSing() ? "Figaro!" : "Shh!";
    },
    toString: function()
    {
        return "Hello, " + this.name + "!";
    }
});

/* Example 4: Define a Ninja class that extends Person */
var Ninja = classify(Person,
{
    constructor: function(name, skillLevel)
    {
        Ninja.base.constructor.call(this, name, true);
        this.skillLevel = skillLevel;
    },
    canSing: function()
    {
        return Ninja.base.canSing.call(this) || this.skillLevel > 200;
    },
    attack: function()
    {
        return "Chop!";
    }
});

/* Example 4: Define an ExtremeNinja class that extends Ninja that extends Person */
var ExtremeNinja = classify(Ninja,
{
    attack: function()
    {
        return "Chop! Chop!";
    },
    backflip: function()
    {
        this.skillLevel++;
        return "Woosh!";
    }
});

var m = new Minimal();
var c = new Class();
var s = new SpanishClass();
var p = new Person("Mary", false);
var n = new Ninja("John", 100);
var e = new ExtremeNinja("World", 200);

And here are my QUnit tests which all pass:

equals(m instanceof Object && m instanceof Minimal && m.constructor === Minimal, true);
equals(c instanceof Object && c instanceof Class && c.constructor === Class, true);
equals(s instanceof Object && s instanceof Class && s instanceof SpanishClass && s.constructor === SpanishClass, true);
equals(p instanceof Object && p instanceof Person && p.constructor === Person, true);
equals(n instanceof Object && n instanceof Person && n instanceof Ninja && n.constructor === Ninja, true);
equals(e instanceof Object && e instanceof Person && e instanceof Ninja && e instanceof ExtremeNinja && e.constructor === ExtremeNinja, true);

equals(c.count(), "John: One. Two. Three.");
equals(s.count(), "Juan: Uno. Dos. Tres.");

equals(p.isQuiet, false);
equals(p.canSing(), true);
equals(p.sing(), "Figaro!");

equals(n.isQuiet, true);
equals(n.skillLevel, 100);
equals(n.canSing(), false);
equals(n.sing(), "Shh!");
equals(n.attack(), "Chop!");

equals(e.isQuiet, true);
equals(e.skillLevel, 200);
equals(e.canSing(), false);
equals(e.sing(), "Shh!");
equals(e.attack(), "Chop! Chop!");
equals(e.backflip(), "Woosh!");
equals(e.skillLevel, 201);
equals(e.canSing(), true);
equals(e.sing(), "Figaro!");
equals(e.toString(), "Hello, World!");

Does anyone see anything wrong with my approach vs. John Resig's original approach? Suggestions and feedback are welcome!

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

Chamomile answered 24/3, 2010 at 18:21 Comment(2)
I would recommend Object.create and traitsjs. Inheritance is no good in javascript, use object compositionLidalidah
Maybe I am just not used to it yet, but the syntax of traits makes my head spin. I think I will pass until it gains a following...Chamomile
U
5

Some time ago, I looked at several object systems for JS and even implemented a few of my own, eg class.js (ES5 version) 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.

Unthoughtof answered 24/3, 2010 at 23:23 Comment(4)
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.Chamomile
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.Chamomile
I was wracking my brain for hours trying to figure out Resig's approach and just could not get it. This is SO straight forward. Thank you!!Orsino
Hang on a minute... how do you define a custom constructor? e.g. set Ninja.strength = function () { Math.random(); } when defining a new Ninja?Orsino
B
4

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();
Bieber answered 24/3, 2010 at 19:5 Comment(4)
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.Chamomile
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.Bieber
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).Chamomile
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"Bieber
B
1

This is about as simple as you can get. It was taken from http://www.sitepoint.com/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];
  }
};
Biogeochemistry answered 24/3, 2010 at 19:35 Comment(4)
It's simple all right, but not as useful (no base/super access), nor pretty IMO.Chamomile
@Will: You can access the parent methods. Check the link for more explanations.Biogeochemistry
@JohnFisher I think you meant Bug.prototype rather than Insert.prototype in the code comments.Ondrej
@LarryBattle: I didn't mean anything, since I didn't create the code. It's from the website.Biogeochemistry

© 2022 - 2024 — McMap. All rights reserved.