simple javaScript inheritance revisited

June 16, 2012 — 1 Comment

… or how to stand on the shoulders of giants.

i’m sure that most of you who are interested in this post will be somewhat familiar with this John Resig post of 2008, where he outlines a simple process for creating object (class?) hierarchies in JavaScript.

as a relative newcomer to JavaScript after 20 years of C/C++/C# development, the last few months have provided a fun challenge. I cut my JavaScript teeth developing the HTML5 ebook From Blue To Red, and that experience provided an excellent opportunity to match my preconceptions of strongly-typed classical inheritance to the strange new world of JavaScript prototypes.

i’m now working on another project which is modeled well with a couple of shallow class hierarchies, and the JavaScript components cannot depend on any other libraries during a bootstrap phase. Resig’s pattern provided an excellent starting point for me, but I ran into trouble when I started using ECMAScript 5 getter and setter properties, which were actually being invoked on reference when copying properties from the object literal to the class prototype.

here then is a version of Resig’s Simple JavaScript Inheritance, revisited and revised for ECMAScript 5 and compliance with “use strict”.


/*
 * Simple JavaScript Inheritance Revisited
 * By Monroe Thomas http://blog.coolmuse.com
 *
 * Based on original code
 *   By John Resig http://ejohn.org/
 *   http://ejohn.org/blog/simple-javascript-inheritance/
 *
 * MIT Licensed.
 *
 */
(function( window ) {

  var classInitializing = false,
    isSuperCalled = /xyz/.test( function() { xyz; } ) ? /\b_super\b/ : /.*/;

  function overrideDescriptorFunction ( key, descriptor, superDescriptor ) {

    var fn = descriptor[key];
    var fnSuper = superDescriptor[key];
    if ( typeof fn === "function" &&
      typeof fnSuper === "function" &&
      isSuperCalled.test( fn ) ) {
      descriptor[key] = (function( fn, fnSuper ) {

        return function() {
          var tmp = this._super;
          this._super = fnSuper;
          try {
            return fn.apply( this, arguments );
          }
          finally {
            this._super = tmp;
          }
        }

      })( fn, fnSuper );

      return true;
    }

    return false;
  }

  // the base class implementation (does nothing)
  window.Class = function() {
  };

  // create a new Class that inherits from this class
  Class.extend = function( prop ) {

    var _super = this.prototype;

    // instantiate a base class (but only create the instance,
    // don't run the init constructor)
    classInitializing = true;
    var prototype = new this();
    classInitializing = false;

    // copy the properties over onto the new prototype
    var properties = Object.getOwnPropertyNames( prop );
    for ( var i = 0, length = properties.length; i < length; i++ ) {

      var name = properties[i];
      var descriptor = Object.getOwnPropertyDescriptor( prop, name );
      var superDescriptor = Object.getOwnPropertyDescriptor( _super, name );

      if ( typeof superDescriptor !== "undefined" ) {

        if ( typeof descriptor.value === "function" ) {

          overrideDescriptorFunction( "value", descriptor, superDescriptor );

        } else {

          var getOverride = false;
          if ( typeof descriptor.get === "function" ) {
            getOverride =
              overrideDescriptorFunction( "get", descriptor, superDescriptor );
          }

          var setOverride = false;
          if ( typeof descriptor.set === "function" ) {
            setOverride =
              overrideDescriptorFunction( "set", descriptor, superDescriptor );
          }

          // if we override a property getter/setter,
          // we have to bring along the corresponding setter/getter
          // if it exists and hasn't also been overridden

          if ( getOverride &&
            !setOverride && typeof superDescriptor.set === "function" ) {
            descriptor.set = (function( fn ) {
              return function() {
                return fn.apply( this, arguments );
              }
            })( superDescriptor.set );
          }

          if ( setOverride &&
            !getOverride && typeof superDescriptor.get === "function" ) {
            descriptor.get = (function( fn ) {
              return function() {
                return fn.apply( this, arguments );
              }
            })( superDescriptor.get );
          }

        }
      }

      Object.defineProperty( prototype, name, descriptor );
    }

    // the dummy class constructor
    function Class () {
      // all construction is actually done in the init method
      if ( !classInitializing && this.init )
        this.init.apply( this, arguments );
    }

    // populate our constructed prototype object
    Class.prototype = prototype;

    // enforce the constructor to be what we expect
    Class.prototype.constructor = Class;

    // and make this class extensible
    Class.extend = window.Class.extend;

    return Class;
  };

})( window );

// tests

var Person = Class.extend( {
  init : function( isDancing ) {
    this.dancing = isDancing;
  },
  dance : function() {
    return this.dancing;
  },
  get danceTrainer () {
    return this.trainerName;
  },
  set danceTrainer ( value ) {
    this.trainerName = value;
  }
} );

var Ninja = Person.extend( {
  init : function() {
    this._super( false );
  },
  dance : function() {
    // Call the inherited version of dance()
    return this._super();
  },
  swingSword : function() {
    return true;
  },
  // override the danceTrainer setter
  set danceTrainer ( value ) {
    this._super( "Master " + value );
  }
} );

var testResult = true;

var p = new Person( true );
testResult = ( true === p.dance() ) || testResult;   // => true
p.danceTrainer = "Baryshnikov";
testResult = ( p.danceTrainer === "Baryshnikov" ) || testResult;

var n = new Ninja();
testResult = ( false === n.dance() ) || testResult;  // => false
testResult = n.swingSword() || testResult;           // => true
n.danceTrainer = "Lee";
testResult = ( n.danceTrainer === "Master Lee" ) || testResult;

// Should all be true
testResult = ( p instanceof Person && p instanceof Class ) || testResult;
testResult = ( n instanceof Ninja && n instanceof Person && n instanceof Class ) || testResult;

alert( "All tests passed: " + testResult );

what luxury to be able to start developing with JavaScript at a time when the major browers are becoming more and more standards compliant!

i believe this code should work on:

  • Opera 11.60
  • Internet Explorer 9
  • Firefox 4
  • Safari 5.1
  • Chrome 13

feedback is welcome!

ciao!

Advertisements

One response to simple javaScript inheritance revisited

  1. 

    Yes. What a luxurious time to be a JavaScript developer. I’ve been looking for an approach that is light weight and prototypical. Something similar to CoffeeScript extends or similar: http://techoctave.com/c7/posts/93-simple-javascript-inheritance-using-coffeescript-extends

    I just really prefer the JavaScript syntax to DSL. But, I love John’s work as well. Thanks!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s