Many authors of JS libraries have made some attempt to emulate familiar Object-Oriented (OO) constructs such as those found in C++ and even more so in Java. The goal is not so much to make JavaScript more Object-oriented, although that is a positive side effect. Rather, it’s to make JavaScript objects more palatable to developers who grew up with OO languages. I demonstrated one possible implementation for creating classes in the Create an Object-oriented JavaScript Class Constructor article. Today, I’d like to continue where we left off last time and add some extensibility to the Class constructor function.
The Class Function Revisited
The Class function takes an object literal (“methods”) which contains the class’s public methods. These are copied to the new class using a technique called method borrowing. Finally, a default constructor is created if there are none defined within the methods object (just as in Java). All classes have code to call their initialize function. These act as the proper OO constructor, not to be confused with the JavaScript object constructor, which encompasses the klass function that contains the call to this.initialize():
var Class = function(methods) { var klass = function() { this.initialize.apply(this, arguments); }; var property; for (property in methods) { klass.prototype[property] = methods[property]; } if (!klass.prototype.initialize) klass.prototype.initialize = function(){}; return klass; }; var Person = Class({ initialize: function(name, age) { this.name = name; this.age = age; }, toString: function() { return "My name is "+this.name+" and I am "+this.age+" years old."; } }); var alice = new Person('Alice', 26); alert(alice.name); //displays "Alice" alert(alice.age); //displays "26" alert(alice.toString()); //displays "My name is Alice and I am 26 years old" in most browsers.
Passing the Parent to the Class() Constructor Function
Any way you slice it, the Class() constructor function needs to know who its daddy is. I personally like the simplicity of adding it as an optional first input parameter. The following Employee class template extends Person and adds the id property and anotherFunction() method:
var Employee = Class(Person, { initialize: function(name, age, id) { //OR put subclass params first: //this.$super('initialize', Array.prototype.slice.call(arguments, 0,2)); this.$super('initialize', name, age); this.id = id; }, toString: function() { return "I am employee #"+this.id+". "+ this.$super('toString'); }, anotherFunction: function() { return 'test'; } });
Accommodating the Optional First Input Parameter
An if statement is required to determine the first argument’s type. It can be one of two types: if the first argument is the parent class, then the typeof operator will identify it as a function type; otherwise, it would evaluate to an object. If the class has a parent, its methods must be copied over to the child. Notice that extend() must still be called to include methods which are overridden by the child class:
var Class = function() { var parent, methods, ,klass = function() { this.initialize.apply(this, arguments); }, extend = function(destination, source) { for (var property in source) { destination[property] = source[property]; } return destination; }; if (typeof arguments[0] === 'function') { parent = arguments[0]; methods = arguments[1]; } else { methods = arguments[0]; } if (parent !== undefined) { extend(klass.prototype, parent.prototype); klass.prototype.$parent = parent.prototype; } extend(klass.prototype, methods); klass.prototype.constructor = klass; if (!klass.prototype.initialize) klass.prototype.initialize = function(){}; return klass; };
That’s Just $super!
In Java, a class can call a parent method using the super keyword. In JavaScript, the super keyword is also reserved for future use, so we can’t use it. Instead, I chose to implement it as a method called $super. You would call it from the child class using this.$super(), passing it the name of the method to execute and arguments:
this.$super('initialize', name, age);
The $super is added in the extend() method, so that every class has access to it. I have seen examples where the author sets it in the klass constructor. That falls apart when you have to traverse more than one level as is frequently the case in constructor chaining. The reason is that the constructor is only called when a class is instantiated using the new keyword. The child class’s parent doesn’t receive it because it is based on the class template, and NOT the instance.
$super() employs a common JavaScript technique for passing method arguments on to another method, calling Array.prototype.slice(). An additional advantage to using slice() is that we can parse out the method argument as well. The Function.apply() method is called on this.$parent[method] so that the parent and arguments can be passed on to the function. The parent must be set as the object on which to apply the parent method as using “this” will cause the function to loop indefinitely!
... extend = function(destination, source) { for (var property in source) { destination[property] = source[property]; } destination.$super = function(method) { return this.$parent[method].apply(this.$parent, Array.prototype.slice.call(arguments, 1)); } return destination; };
Copying Method Properties
It is not enough to copy methods from the parent to the child class; properties must also be copied over. This is because the initialization code that creates and sets class properties belongs to the parent that owns the initialize() function.
The ideal place to copy properties is in the klass constructor because it has access to both the property names and their values. A regular expression parses out the parameter list from the function source code. From there, the argument list is split on the commas and iterated over to set each property:
klass = function() { this.initialize.apply(this, arguments); //copy the properties so that they can be called directly from the child //class without $super, i.e., this.name var reg = /(([sS]*?))/; var params = reg.exec(this.initialize.toString()); if (params) { var param_names = params[1].split(','); for ( var i=0; i<param_names.length; i++ ) { this[param_names[i]] = arguments[i]; } } }
Here is a working demo file with today’s code.
Conclusion
Another approach I considered for accessing properties was to use getter and setter methods rather than allow child classes to access properties directly using the this pointer. Ultimately, the solution you use depends on how available you want to makes your class attributes. Accessing them directly mimics the protected access modifier in Java, while forcing the use of getters and setters treats properties more like private class members.