… 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!
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!