Implementing Javascript Inheritance And Synthesized Accessors With Annotation

Posted October 22, 2010 at 10:04 AM by Ben Nadel

Tags: Javascript / DHTML

Yesterday, I explored multi-class inheritance in Javascript through the use of Class prototypes. Unlike some of my previous sub-classing attempts, however, this approach copied the base class prototype methods rather than trying to extend a base class instance. While I liked where this was going, I wasn't crazy about the way that it looked. As such, I wanted to see if I could get the same kind of functionality using a more annotation-based approach. This way, I could "describe" how the class was supposed to function without having to work so closely with the implementation.

 
 
 
 
 
 
 
 
 
 

For this experiment, I wanted to annotate the class constructor with two properties:

  • Class.extends = Class || [ Class ]
  • Class.properties = [ ]

The first annotation, "extends," points to the base class (or classes) that our sub-class is going to extend. This can be either a single base class or an array of base classes. If you provide an array, the classes will be extended in the order in which they were annotated. This will give the last class the highest precedence.

The second annotation, "properties," is simply an array of property names for which we want to synthesize accessor methods (getters and setters). So, for example, if the properties value was:

  • Class.properties = [ "name", "age" ];

... we would automatically synthesize the following accessor methods for our class:

  • getName()
  • setName( value )
  • getAge()
  • setAge( value )

Now, as with anything that is annotation-based, you actually need a secondary system that will build out the target system based on the given annotations. In my demo, I created the finalizeClass() function to do the building. After you have annotated your class, you have to pass it to the finalizeClass() function which will take care of turning the "extends" and "properties" annotations into actual class methods.

To see this in action, let's take a look at the following demo. In this code, I have a Ben class that extends a Person class (NOTE: The first half of this is just the finalizeClass() function so you might want to skip the bottom half first):

  • <!DOCTYPE html>
  • <html>
  • <head>
  • <title>Javascript Inheritance And Synthesized Accessors With Annotation</title>
  • <script type="text/javascript" src="./jquery-1.4.3.js"></script>
  • </head>
  • <body>
  •  
  • <h1>
  • Javascript Inheritance And Synthesized Accessors With Annotation
  • </h1>
  •  
  •  
  • <script type="text/javascript">
  •  
  • // I "build" classes based on constructor met data - I finish
  • // the extension and class method synthesis.
  • function finalizeClass( classDefinition ){
  • // We are going to need to override the current prototype
  • // with any extended class methods and synthesized
  • // accessor methods. Let's start out with an empty
  • // aggregation.
  • var classMethods = {};
  •  
  • // Check to see if this class extends any other classes.
  • if ("extends" in classDefinition){
  •  
  • // Make sure the extends is an array.
  • if (!$.isArray( classDefinition.extends )){
  •  
  • // Convert to an array for unified access.
  • classDefinition.extends = [ classDefinition.extends ];
  •  
  • }
  •  
  • // Loop over each class in the "extends" property and
  • // add it's class methods to the aggregate.
  • $.each(
  • classDefinition.extends,
  • function( classIndex, baseClass ){
  •  
  • // Make sure this base class is finalized
  • // (that is has been built-out).
  • if (!baseClass.isFinalized){
  •  
  • // Finalize this class before we try to
  • // use it to augment our sub-class.
  • finalizeClass( baseClass );
  •  
  • }
  •  
  • // Add the base class methods to the
  • // current aggregate.
  • classMethods = $.extend(
  • classMethods,
  • baseClass.prototype
  • );
  •  
  • }
  • );
  •  
  • }
  •  
  • // Now that we've copied any base class prototype
  • // methods to our aggregated method collection,let's
  • // add the target class methods to the collectin of
  • // class methods. This will give our target method the
  • // highest presedence which is what we want.
  • classMethods = $.extend(
  • classMethods,
  • classDefinition.prototype
  • );
  •  
  • // I take a property name and Uppercase the first letter,
  • // getting it ready for an accessor name.
  • var prepareAccessorName = function( accessor, propertyName ){
  • return(
  • accessor +
  • propertyName.replace(
  • new RegExp( "^[a-z]", "" ),
  • function( $0 ){
  • return( $0.toUpperCase() );
  • }
  • )
  • );
  • };
  •  
  • // Loop over any properties to synthesize accessors
  • // (getter and setter methods). This assumes that the
  • // class is using lower-camel-case formatting.
  • $.each(
  • (classDefinition.properties || []),
  • function( index, propertyName ){
  •  
  • // Create the accessor method names.
  • var getterName = prepareAccessorName( "get", propertyName );
  • var setterName = prepareAccessorName( "set", propertyName );
  •  
  • // Check to make sure this method doesn't already
  • // exists - we only want to synthesize it if it
  • // isn't concrete.
  • if (!(getterName in classMethods)){
  •  
  • // Add this getter to the prototype.
  • classMethods[ getterName ] = function(){
  • return( this[ propertyName ] );
  • };
  •  
  • }
  •  
  • // Check to make sure this method doesn't already
  • // exists - we only want to synthesize it if it
  • // isn't concrete.
  • if (!(setterName in classMethods)){
  •  
  • // Add this setter to the prototype.
  • classMethods[ setterName ] = function( value ){
  • // Store the new property.
  • this[ propertyName ] = value;
  •  
  • // Return this object for method chaining.
  • return( this );
  • };
  •  
  • }
  •  
  • }
  • );
  •  
  • // Now that we've created our entire prototype collection
  • // including any synthesize accessors, let's store the
  • // class methods back into our target class prototype.
  • classDefinition.prototype = classMethods;
  •  
  • // Flag this class at finalized.
  • classDefinition.isFinalized = true;
  • }
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // I am the Person class constructor.
  • function Person( name, age ){
  • this.setName( name );
  • this.setAge( age );
  • }
  •  
  • // Define the properties for which we want to synthesize
  • // accessor methods (getters and setters).
  • Person.properties = [ "name", "age" ];
  •  
  • // Define the actual class methods.
  • Person.prototype = {
  •  
  • // I say hello!
  • sayHello: function(){
  • return( "Hello, I'm " + this.getName() );
  • }
  •  
  • };
  •  
  • // Build out the class definition.
  • finalizeClass( Person );
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // I am the Ben class constructor.
  • function Ben(){
  • // Call the super constructor.
  • Person.call( this, "Ben", 30 );
  • }
  •  
  • // Define the class extension. This can be a single value or
  • // an array of classes.
  • Ben.extends = [ Person ];
  •  
  • // Define the actual class methods.
  • Ben.prototype = {
  •  
  • // I give high-fives!
  • highFive: function(){
  • return( "Oh yeah!" );
  • }
  •  
  • };
  •  
  • // Build out the class definition.
  • finalizeClass( Ben );
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // Create an instance of the Ben class.
  • var ben = new Ben();
  •  
  • // Test the class methods.
  • console.log( "Name:", ben.getName() );
  • console.log( "Age:", ben.getAge() );
  • console.log( "High-Five:", ben.highFive() );
  •  
  • // Test to see if the setters work.
  • console.log( "Setting...." );
  •  
  • // Set properties via synthesized accessors.
  • ben.setName( "B-Jamin" );
  • ben.setAge( 21 );
  •  
  • // Test the class methods again to see if setters worked.
  • console.log( "Name:", ben.getName() );
  • console.log( "Age:", ben.getAge() );
  •  
  • </script>
  •  
  • </body>
  • </html>

As you can see, we have a Person class that has the properties "name" and "age". Then, we have our Ben class which extends the Person class. After we pass both classes to the finalizeClass() function and run our test code, we get the following console output:

Name: Ben
Age: 30
High-Five: Oh yeah!

Setting....

Name: B-Jamin
Age: 21

Works like a charm. And, what's nice is that all of the method synthesis is done before any of the classes are ever instantiated. This means that you can use the new accessor methods even from within the class constructors (as I am doing in the Person constructor with setName() and setAge()).

One other thing that you might notice, if you look at the finalizeClass() function, is that if the target class already defines a given accessor, the finalizeClass() function won't overwrite it. As such, I can always add my own setters or getters when additional business logic needs to be applied without having to worry about changing the properties list.

There's something about working directly with the Javascript class definitions that I like; so, I don't want to entirely abstract-away my class construction. But, I am not against using annotation to help augment the prototype and build accessor methods that have no inherent business logic.




Reader Comments

Oct 22, 2010 at 1:47 PM // reply »
8 Comments

That is superb! I've just been recently started creating JavaScript classes (so I'm still pretty new to it), and that cuts out a lot of work in the long run with the setup of classes.

If you wanted to go down the route that ColdFusion did with implicit getters and setters, you could make the property definition take more information to let the finalizeClass function decide how to create the functions.

Using your example as an example:

  • // Define the properties for which we want to synthesize
  • // accessor methods (getters and setters).
  • Person.properties = [
  • {
  • name: "name",
  • getter: true,
  • setter: false
  • },
  • {
  • name: "age",
  • getter: true,
  • setter: true
  • }
  • ];

The finalizeClass function could then be modified to use the object instead of just the name to suppress creating a getter or setter if that was desired for a specific class (in case you wanted the only way to set the value of a property was on init).

You could even support type validation and create more robust setter functions.

The only downside to the "getter" and "setter" attributes is that the properties themselves are being stored in the "this" scope of the class, which makes them publicly available on their own if I am remembering correctly. But I am wondering if there is a way to support properties that are only publicly available through the getters and setters and still allow you to describe the class in the method you are using above (or similar to at least).


Oct 22, 2010 at 1:48 PM // reply »
8 Comments

Ugh, it's too early for me to write proper sentences, sorry about the poor grammar.


Oct 22, 2010 at 3:20 PM // reply »
11,238 Comments

@Mike,

It's funny you mention that. When I first started coding the demo this morning, I was going to define getters and setters as separate lists:

Person.getters = [ "name" ];
Person.setters = [ "name", "age" ];

But, the more I thought about it, the less I figured I was actually ever going to differentiate between the two; as such, I just decided to merge them down into a single list - properties.

Plus, as you say, this all goes into the "this" scope which is public anyway. Of course, Javascript doesn't really have the sense of a private scope. People use the "module" pattern to create private variables, but I just can't seem to use the module pattern without feeling... ungood. It just requires you to know too much about how the object is coded. Then some variables are scoped, some are not.

Ultimately, I prefer uniformity over a philosophical separation of public and private. But, that is just my personal preference. I know a lot of people who LOVE the module pattern. It's just not for me.


Dec 6, 2010 at 6:22 AM // reply »
1 Comments

good stuff,
Q - why do you use the propertyName.replace(...
and not just:
propertyName[0].toUpperCase()
return accessor + propertyName;

seems to be more readable


Post A Comment

Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.

Please review the following issues:

Author Name:


Author Email:

Author Website:

Comment:

Supported HTML tags for formatting: <strong>bold</strong>   <em>italic</em>   <code>code</code>







  • Help Wanted - Find Your Next ColdFusion Job
Ben Nadel's Company - Epicenter Consulting Recent Blog Comments
May 17, 2013 at 7:42 PM
HashKeyCopier - An AngularJS Utility Class For Merging Cached And Live Data
Ben - thanks so much for posting these Angular articles and findings, they've been a huge help towards learning one of the more 'complex' JavaScript frameworks out there (IMO). I have been using Angu ... read »
May 16, 2013 at 5:01 PM
UPDATE: Parsing CSV Data Files In ColdFusion With csvToArray()
Your code was the closest thing I've found to obtaining some direction for converting ISO fields to values that CF can translate properly. Thank you for posting! ... read »
May 15, 2013 at 10:37 PM
Very Simple Pusher And ColdFusion Powered Chat
hi id making plz easy ... read »
May 15, 2013 at 6:07 PM
Making SOAP Web Service Requests With ColdFusion And CFHTTP
Ben, you once again saved my bacon at work. Thank you, thank you, thank you! ... read »
May 15, 2013 at 4:15 PM
What If All User Interface (UI) Data Came In Reports?
@Josh, Thanks! @Ben, I definitely recommend the David West book "Object Thinking" I've been quoting from. It goes deeply into the philosophy and history of OO programming. His breadth ... read »
May 15, 2013 at 11:36 AM
Ask Ben: Print Part Of A Web Page With jQuery
I found this helpfull when you need to keep (refresh) the original parent page after closing the iframe child print dialog (Hoping you're not using a form at this time so it won't submit again): On ... read »
May 14, 2013 at 7:13 PM
What If All User Interface (UI) Data Came In Reports?
@Jonah, If there's any books you'd recommend on the subject of domain modelling, I'd love to hear it. I just downloaded the free PDF of "Domain Driven Design Quickly". Figured I'd give it ... read »
May 14, 2013 at 6:57 PM
The UX Of Prototyping: Low-Fidelity Is The New High-Fidelity
@Phillip, I'm not sure I follow what you mean? Are you saying that you looked at the list of widgets provided by the jQuery UI and let that be your style guide? ... read »
InVision App - Prototyping Made Beautiful With Prototyping Tools