56
return s; // Return the new set
};
The appeal of factory methods here is that you can give them whatever name you want,
and methods with different names can perform different kinds of initializations. Since
constructors serve as the public identity of a class, however, there is usually only a single
constructor per class. This is not a hard-and-fast rule, however. In JavaScript it is pos-
sible to define multiple constructor functions that share a single prototype object, and
if you do this, objects created by any of the constructors will be of the same type. This
technique is not recommended, but here is an auxiliary constructor of this type:
// An auxiliary constructor for the Set class.
function SetFromArray(a) {
// Initialize new object by invoking Set() as a function,
// passing the elements of a as individual arguments.
Set.apply(this, a);
}
// Set the prototype so that SetFromArray creates instances of Set
SetFromArray.prototype = Set.prototype;
var s = new SetFromArray([1,2,3]);
s instanceof Set // => true
In ECMAScript 5, the
bind()
method of functions has special behavior that allows it
to create this kind of auxiliary constructor. See §8.7.4.
9.7 Subclasses
In object-oriented programming, a class B can extend or subclass another class A. We
say that A is the superclass and B is the subclass. Instances of B inherit all the instance
methods of A. The class B can define its own instance methods, some of which may
override methods of the same name defined by class A. If a method of B overrides a
method of A, the overriding method in B may sometimes want to invoke the overridden
method in A: this is called method chaining. Similarly, the subclass constructor
B()
may
sometimes need to invoke the superclass constructor
A()
. This is called constructor
chaining. Subclasses can themselves have subclasses, and when working with hierar-
chies of classes, it can sometimes be useful to define abstract classes. An abstract class
is one that defines one or more methods without an implementation. The implemen-
tation of these abstract methods is left to the concrete subclasses of the abstract class.
The key to creating subclasses in JavaScript is proper initialization of the prototype
object. If class B extends A, then
B.prototype
must be an heir of
A.prototype
. Then
instances of B will inherit from
B.prototype
which in turn inherits from
A.prototype
.
This section demonstrates each of the subclass-related terms defined above, and also
covers an alternative to subclassing known as composition.
Using the Set class of Example 9-6 as a starting point, this section will demonstrate how
to define subclasses, how to chain to constructors and overridden methods, how to use
composition instead of inheritance, and finally, how to separate interface from imple-
228 | Chapter 9: Classes and Modules
53
mentation with abstract classes. The section ends with an extended example that de-
fines a hierarchy of Set classes. Note that the early examples in this section are intended
to demonstrate basic subclassing techniques. Some of these examples have important
flaws that will be addressed later in the section.
9.7.1 Defining a Subclass
JavaScript objects inherit properties (usually methods) from the prototype object of
their class. If an object O is an instance of a class B and B is a subclass of A, then O
must also inherit properties from A. We arrange this by ensuring that the prototype
object of B inherits from the prototype object of A. Using our
inherit()
function
(Example 6-1), we write:
B.prototype = inherit(A.prototype); // Subclass inherits from superclass
B.prototype.constructor = B; // Override the inherited constructor prop.
These two lines of code are the key to creating subclasses in JavaScript. Without them,
the prototype object will be an ordinary object—an object that inherits from
Object.prototype
—and this means that your class will be a subclass of Object like all
classes are. If we add these two lines to the
defineClass()
function (from §9.3), we can
transform it into the
defineSubclass()
function and the
Function.proto
type.extend()
method shown in Example 9-11.
Example 9-11. Subclass definition utilities
// A simple function for creating simple subclasses
function defineSubclass(superclass, // Constructor of the superclass
constructor, // The constructor for the new subclass
methods, // Instance methods: copied to prototype
statics) // Class properties: copied to constructor
{
// Set up the prototype object of the subclass
constructor.prototype = inherit(superclass.prototype);
constructor.prototype.constructor = constructor;
// Copy the methods and statics as we would for a regular class
if (methods) extend(constructor.prototype, methods);
if (statics) extend(constructor, statics);
// Return the class
return constructor;
}
// We can also do this as a method of the superclass constructor
Function.prototype.extend = function(constructor, methods, statics) {
return defineSubclass(this, constructor, methods, statics);
};
Example 9-12 demonstrates how to write a subclass “manually” without using the
defineSubclass()
function. It defines a SingletonSet subclass of Set. A SingletonSet is
a specialized set that is read-only and has a single constant member.
9.7 Subclasses | 229
Core JavaScript
65
Example 9-12. SingletonSet: a simple set subclass
// The constructor function
function SingletonSet(member) {
this.member = member; // Remember the single member of the set
}
// Create a prototype object that inherits from the prototype of Set.
SingletonSet.prototype = inherit(Set.prototype);
// Now add properties to the prototype.
// These properties override the properties of the same name from Set.prototype.
extend(SingletonSet.prototype, {
// Set the constructor property appropriately
constructor: SingletonSet,
// This set is read-only: add() and remove() throw errors
add: function() { throw "read-only set"; },
remove: function() { throw "read-only set"; },
// A SingletonSet always has size 1
size: function() { return 1; },
// Just invoke the function once, passing the single member.
foreach: function(f, context) { f.call(context, this.member); },
// The contains() method is simple: true only for one value
contains: function(x) { return x === this.member; }
});
Our SingletonSet class has a very simple implementation that consists of five simple
method definitions. It implements these five core Set methods, but inherits methods
such as
toString()
,
toArray()
and
equals()
from its superclass. This inheritance of
methods is the reason for defining subclasses. The
equals()
method of the Set class
(defined in §9.6.4), for example, works to compare any Set instance that has working
size()
and
foreach()
methods with any Set that has working
size()
and
contains()
methods. Because SingletonSet is a subclass of Set, it inherits this
equals()
implemen-
tation automatically and doesn’t have to write its own. Of course, given the radically
simple nature of singleton sets, it might be more efficient for SingletonSet to define its
own version of
equals()
:
SingletonSet.prototype.equals = function(that) {
return that instanceof Set && that.size()==1 && that.contains(this.member);
};
Note that SingletonSet does not statically borrow a list of methods from Set: it dynam-
ically inherits the methods of the Set class. If we add a new method to
Set.prototype
,
it immediately becomes available to all instances of Set and of SingletonSet (assuming
SingletonSet does not already define a method by the same name).
9.7.2 Constructor and Method Chaining
The SingletonSet class in the last section defined a completely new set implementation,
and completely replaced the core methods it inherited from its superclass. Often, how-
ever, when we define a subclass, we only want to augment or modify the behavior of
our superclass methods, not replace them completely. To do this, the constructor and
230 | Chapter 9: Classes and Modules
62
methods of the subclass call or chain to the superclass constructor and the superclass
methods.
Example 9-13 demonstrates this. It defines a subclass of Set named NonNullSet: a set
that does not allow
null
and
undefined
as members. In order to restrict the membership
in this way, NonNullSet needs to test for null and undefined values in its
add()
method.
But it doesn’t want to reimplement the
add()
method completely, so it chains to the
superclass version of the method. Notice also that the
NonNullSet()
constructor doesn’t
take any action of its own: it simply passes its arguments to the superclass constructor
(invoking it as a function, not as a constructor) so that the superclass constructor can
initialize the newly created object.
Example 9-13. Constructor and method chaining from subclass to superclass
/*
* NonNullSet is a subclass of Set that does not allow null and undefined
* as members of the set.
*/
function NonNullSet() {
// Just chain to our superclass.
// Invoke the superclass constructor as an ordinary function to initialize
// the object that has been created by this constructor invocation.
Set.apply(this, arguments);
}
// Make NonNullSet a subclass of Set:
NonNullSet.prototype = inherit(Set.prototype);
NonNullSet.prototype.constructor = NonNullSet;
// To exclude null and undefined, we only have to override the add() method
NonNullSet.prototype.add = function() {
// Check for null or undefined arguments
for(var i = 0; i < arguments.length; i++)
if (arguments[i] == null)
throw new Error("Can't add null or undefined to a NonNullSet");
// Chain to the superclass to perform the actual insertion
return Set.prototype.add.apply(this, arguments);
};
Let’s generalize this notion of a non-null set to a “filtered set”: a set whose members
must pass through a filter function before being added. We’ll define a class factory
function (like the
enumeration()
function from Example 9-7) that is passed a filter
function and returns a new Set subclass. In fact, we can generalize even further and
define our class factory to take two arguments: the class to subclass and the filter to
apply to its
add()
method. We’ll call this factory method
filteredSetSubclass()
, and
we might use it like this:
// Define a set class that holds strings only
var StringSet = filteredSetSubclass(Set,
function(x) {return typeof x==="string";});
// Define a set class that does not allow null, undefined or functions
9.7 Subclasses | 231
Core JavaScript
53
var MySet = filteredSetSubclass(NonNullSet,
function(x) {return typeof x !== "function";});
The code for this class factory function is in Example 9-14. Notice how this function
performs the same method and constructor chaining as NonNullSet did.
Example 9-14. A class factory and method chaining
/*
* This function returns a subclass of specified Set class and overrides
* the add() method of that class to apply the specified filter.
*/
function filteredSetSubclass(superclass, filter) {
var constructor = function() { // The subclass constructor
superclass.apply(this, arguments); // Chains to the superclass
};
var proto = constructor.prototype = inherit(superclass.prototype);
proto.constructor = constructor;
proto.add = function() {
// Apply the filter to all arguments before adding any
for(var i = 0; i < arguments.length; i++) {
var v = arguments[i];
if (!filter(v)) throw("value " + v + " rejected by filter");
}
// Chain to our superclass add implementation
superclass.prototype.add.apply(this, arguments);
};
return constructor;
}
One interesting point to note about Example 9-14 is that by wrapping a function around
our subclass creation code, we are able to use the
superclass
argument in our con-
structor and method chaining code rather than hard-coding the name of the actual
superclass. This means that if we wanted to change the superclass, we would only have
to change it in one spot, rather than searching our code for every mention of it. This is
arguably a technique that is worth using, even if we’re not defining a class factory. For
example, we could rewrite our
NonNullSet
using a wrapper function and the
Function.prototype.extend()
method (of Example 9-11) like this:
var NonNullSet = (function() { // Define and invoke function
var superclass = Set; // Only specify the superclass once.
return superclass.extend(
function() { superclass.apply(this, arguments); }, // the constructor
{ // the methods
add: function() {
// Check for null or undefined arguments
for(var i = 0; i < arguments.length; i++)
if (arguments[i] == null)
throw new Error("Can't add null or undefined");
// Chain to the superclass to perform the actual insertion
return superclass.prototype.add.apply(this, arguments);
}
232 | Chapter 9: Classes and Modules
Documents you may be interested
Documents you may be interested