Skip to main content
Ben Nadel at the Angular NYC Meetup (Sep. 2018) with: Roman Ilyushin
Ben Nadel at the Angular NYC Meetup (Sep. 2018) with: Roman Ilyushin

Implementing Javascript Inheritance And Synthesized Accessors With Annotation

By
Published in Comments (4)

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.

Want to use code from this post? Check out the license.

Reader Comments

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).

15,902 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.

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

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel