9.5. Superclasses and SubclassesJava, C++, and other class-based object-oriented languages have an explicit concept of the class hierarchy. Every class can have a superclass from which it inherits properties and methods. Any class can be extended, or subclassed, so that the resulting subclass inherits its behavior. As shown previously, JavaScript supports prototype inheritance instead of class-based inheritance. Still, JavaScript analogies to the class hierarchy can be drawn. In JavaScript, the Object class is the most generic, and all other classes are specialized versions, or subclasses, of it. Another way to say this is that Object is the superclass of all the built-in classes, and all classes inherit a few basic methods from Object. Recall that objects inherit properties from the prototype object of their constructor. How do they also inherit properties from the Object class? Remember that the prototype object is itself an object; it is created with the Object( ) constructor. This means the prototype object itself inherits properties from Object.prototype! Prototype-based inheritance is not limited to a single prototype object; instead, a chain of prototype objects is involved. Thus, a Complex object inherits properties from Complex.prototype and from Object.prototype. When you look up a property in a Complex object, the object itself is searched first. If the property is not found, the Complex.prototype object is searched next. Finally, if the property is not found in that object, the Object.prototype object is searched. Note that because the Complex prototype object is searched before the Object prototype object, properties of Complex.prototype hide any properties with the same name in Object.prototype. For example, in the Complex class of Example 9-2, a toString( ) method was defined in the Complex.prototype object. Object.prototype also defines a method with this name, but Complex objects never see it because the definition of toString( ) in Complex.prototype is found first. The classes shown so far in this chapter are all direct subclasses of Object. When necessary, however, it is possible to subclass any other class. Recall the Rectangle class shown earlier in the chapter, for example. It had properties that represent the width and height of the rectangle but no properties describing its position. Suppose you want to create a subclass of Rectangle in order to add fields and methods related to the position of the rectangle. To do this, simply make sure that the prototype object of the new class is itself an instance of Rectangle so that it inherits all the properties of Rectangle.prototype. Example 9-3 repeats the definition of a simple Rectangle class and then extends it to define a PositionedRectangle class. Example 9-3. Subclassing a JavaScript class
As you can see from Example 9-3, creating a subclass in JavaScript is not as simple as creating a class that inherits directly from Object. First, there is the issue of invoking the superclass constructor from the subclass constructor. Take care when you do this that the superclass constructor is invoked as a method of the newly created object. Next, there are the tricks required to set the prototype object of the subclass constructor. You must explicitly create this prototype object as an instance of the superclass, then explicitly set the constructor property of the prototype object.[*] Optionally, you may also want to delete any properties that the superclass constructor created in the prototype object because what's important are the properties that the prototype object inherits from its prototype.
Having defined this PositionedRectangle class, you can use it with code like this: var r = new PositionedRectangle(2,2,2,2); print(r.contains(3,3)); // invoke an instance method print(r.area( )); // invoke an inherited instance method // Use the instance fields of the class: print(r.x + ", " + r.y + ", " + r.width + ", " + r.height); // Our object is an instance of all 3 of these classes print(r instanceof PositionedRectangle && r instanceof Rectangle && r instanceof Object); 9.5.1. Constructor ChainingIn the example just shown, the PositionedRectangle( ) constructor function needed to explicitly invoke the superclass constructor function. This is called constructor chaining and is quite common when creating subclasses. You can simplify the syntax for constructor chaining by adding a property named superclass to the prototype object of the subclass: // Store a reference to our superclass constructor. PositionedRectangle.prototype.superclass = Rectangle; With this property defined, the syntax for constructor chaining is simpler: function PositionedRectangle(x, y, w, h) { this.superclass(w,h); this.x = x; this.y = y; } Note that the superclass constructor function is explicitly invoked through the this object. This means that you no longer need to use call( ) or apply( ) to invoke the superclass constructor as a method of that object. 9.5.2. Invoking Overridden MethodsWhen a subclass defines a method that has the same name as a method in the superclass, the subclass overrides that method. This is a relatively common thing to do when creating subclasses of existing classes. Anytime you define a toString( ) method for a class, you override the toString( ) method of Object, for example. A method that overrides another often wants to augment the functionality of the overridden method instead of replacing it altogether. To do that, a method must be able to invoke the method that it overrides. In a sense, this is a kind of method chaining, as described for constructors earlier in this chapter. Invoking an overridden method is more awkward than invoking a superclass constructor, however. Let's consider an example. Suppose the Rectangle class had defined a toString( ) method (as it should have in the first place): Rectangle.prototype.toString = function( ) { return "[" + this.width + "," + this.height + "]"; } If you give Rectangle a toString( ) method, you really must override that method in PositionedRectangle so that instances of the subclass have a string representation that reflects all their properties, not just their width and height properties. PositionedRectangle is a simple enough class that its toString( ) method can just return the values of all properties. But for the sake of example, let's handle the position properties and delegate to its superclass for the width and height properties. Here is what the code might look like: PositionedRectangle.prototype.toString = function( ) { return "(" + this.x + "," + this.y + ") " + // our fields Rectangle.prototype.toString.apply(this); // chain to superclass } The superclass's implementation of toString( ) is a property of the superclass's prototype object. Note that you can't invoke it directly. Invoke it with apply( ) so that you can specify the object on which it should be called. If you add a superclass property to PositionedRectangle.prototype, this code can be rewritten in a superclass-independent way: PositionedRectangle.prototype.toString = function( ) { return "(" + this.x + "," + this.y + ") " + // our fields this.superclass.prototype.toString.apply(this); } |