loading...

JavaScript – Object-Oriented Techniques in JavaScript

So far in this chapter we’ve covered the architectural
fundamentals of classes in JavaScript: the importance of the prototype
object, its connections to the constructor function, how the instanceof operator works, and so on. In
this section we switch gears and demonstrate a number of practical
(though not fundamental) techniques for programming with JavaScript
classes. We begin with two nontrivial example classes that are
interesting in their own right but also serve as starting points for
the discussions that follow.

Example: A Set Class

A set is a data structure that represents
an unordered collection of values, with no duplicates. The
fundamental operations on sets are adding values and testing whether
a value is a member of the set, and sets are generally implemented
so that these operations are fast. JavaScript’s objects are
basically sets of property names, with values associated with each
name. It is trivial, therefore, to use an object as a set of
strings. Example 9-6 implements a more general Set
class in JavaScript. It works by mapping any JavaScript value to a
unique string, and then using that string as a property name.
Objects and functions do not have a concise and reliably unique
string representation, so the Set class must define an identifying
property on any object or function stored in the set.

Example 9-6. Set.js: An arbitrary set of values

function Set() {          // This is the constructor
    this.values = {};     // The properties of this object hold the set
    this.n = 0;           // How many values are in the set
    this.add.apply(this, arguments);  // All arguments are values to add
}

// Add each of the arguments to the set.
Set.prototype.add = function() {
    for(var i = 0; i < arguments.length; i++) {  // For each argument
        var val = arguments[i];                  // The value to add to the set
        var str = Set._v2s(val);                 // Transform it to a string
        if (!this.values.hasOwnProperty(str)) {  // If not already in the set
            this.values[str] = val;              // Map string to value
            this.n++;                            // Increase set size
        }
    }
    return this;                                 // Support chained method calls
};

// Remove each of the arguments from the set.
Set.prototype.remove = function() {
    for(var i = 0; i < arguments.length; i++) {  // For each argument
        var str = Set._v2s(arguments[i]);        // Map to a string
        if (this.values.hasOwnProperty(str)) {   // If it is in the set
            delete this.values[str];             // Delete it
            this.n--;                            // Decrease set size
        }
    }
    return this;                                 // For method chaining
};

// Return true if the set contains value; false otherwise.
Set.prototype.contains = function(value) {
    return this.values.hasOwnProperty(Set._v2s(value));
};

// Return the size of the set.
Set.prototype.size = function() { return this.n; };

// Call function f on the specified context for each element of the set.
Set.prototype.foreach = function(f, context) {
    for(var s in this.values)                 // For each string in the set
        if (this.values.hasOwnProperty(s))    // Ignore inherited properties
            f.call(context, this.values[s]);  // Call f on the value
};

// This internal function maps any JavaScript value to a unique string.
Set._v2s = function(val) {
    switch(val) {
        case undefined:     return 'u';          // Special primitive
        case null:          return 'n';          // values get single-letter
        case true:          return 't';          // codes.
        case false:         return 'f';
        default: switch(typeof val) {
            case 'number':  return '#' + val;    // Numbers get # prefix.
            case 'string':  return '"' + val;    // Strings get " prefix.
            default: return '@' + objectId(val); // Objs and funcs get @
        }
    }

    // For any object, return a string. This function will return a different
    // string for different objects, and will always return the same string
    // if called multiple times for the same object. To do this it creates a
    // property on o. In ES5 the property would be nonenumerable and read-only.
    function objectId(o) {
        var prop = "|**objectid**|";   // Private property name for storing ids
        if (!o.hasOwnProperty(prop))   // If the object has no id
            o[prop] = Set._v2s.next++; // Assign it the next available
        return o[prop];                // Return the id
    }
};
Set._v2s.next = 100;    // Start assigning object ids at this value.

Example: Enumerated Types

An enumerated type is a type with a
finite set of values that are listed (or “enumerated”) when the type
is defined. In C and languages derived from it, enumerated types are
declared with the enum keyword.
enum is a reserved (but unused)
word in ECMAScript 5 which leaves open the
possibility that JavaScript may someday have native enumerated
types. Until then, Example 9-7 shows how you
can define your own enumerated types in JavaScript. Note that it
uses the inherit() function from
Example 6-1
.

Example 9-7 consists of a single
function enumeration(). This is
not a constructor function, however: it does not define a class
named “enumeration”. Instead, this is a factory function: each
invocation creates and returns a new class. Use it like this:

// Create a new Coin class with four values: Coin.Penny, Coin.Nickel, etc.
var Coin = enumeration({Penny: 1, Nickel:5, Dime:10, Quarter:25});
var c = Coin.Dime;                   // This is an instance of the new class
c instanceof Coin                    // => true: instanceof works
c.constructor == Coin                // => true: constructor property works
Coin.Quarter + 3*Coin.Nickel         // => 40: values convert to numbers
Coin.Dime == 10                      // => true: more conversion to numbers
Coin.Dime > Coin.Nickel              // => true: relational operators work
String(Coin.Dime) + ":" + Coin.Dime  // => "Dime:10": coerce to string

The point of this example is to demonstrate that JavaScript
classes are much more flexible and dynamic than the static classes
of languages like C++ and Java.

Example 9-7. Enumerated types in JavaScript

// This function creates a new enumerated type.  The argument object specifies
// the names and values of each instance of the class. The return value
// is a constructor function that identifies the new class.  Note, however
// that the constructor throws an exception: you can't use it to create new
// instances of the type.  The returned constructor has properties that 
// map the name of a value to the value itself, and also a values array,
// a foreach() iterator function
function enumeration(namesToValues) {
    // This is the dummy constructor function that will be the return value.
    var enumeration = function() { throw "Can't Instantiate Enumerations"; };

    // Enumerated values inherit from this object.
    var proto = enumeration.prototype = {
        constructor: enumeration,                   // Identify type
        toString: function() { return this.name; }, // Return name
        valueOf: function() { return this.value; }, // Return value
        toJSON: function() { return this.name; }    // For serialization
    };

    enumeration.values = [];  // An array of the enumerated value objects

    // Now create the instances of this new type.
    for(name in namesToValues) {         // For each value 
        var e = inherit(proto);          // Create an object to represent it
        e.name = name;                   // Give it a name
        e.value = namesToValues[name];   // And a value
        enumeration[name] = e;           // Make it a property of constructor
        enumeration.values.push(e);      // And store in the values array
    }
    // A class method for iterating the instances of the class
    enumeration.foreach = function(f,c) {
        for(var i = 0; i < this.values.length; i++) f.call(c,this.values[i]);
    };

    // Return the constructor that identifies the new type
    return enumeration;
}

The “hello world” of enumerated types is to use an enumerated
type to represent the suits in a deck of cards. Example 9-8 uses the enumeration() function in this way and
also defines classes to represents cards and decks of
cards.[15]

Example 9-8. Representing cards with enumerated types

// Define a class to represent a playing card
function Card(suit, rank) {
    this.suit = suit;         // Each card has a suit
    this.rank = rank;         // and a rank
}

// These enumerated types define the suit and rank values
Card.Suit = enumeration({Clubs: 1, Diamonds: 2, Hearts:3, Spades:4});
Card.Rank = enumeration({Two: 2, Three: 3, Four: 4, Five: 5, Six: 6,
                         Seven: 7, Eight: 8, Nine: 9, Ten: 10,
                         Jack: 11, Queen: 12, King: 13, Ace: 14});

// Define a textual representation for a card
Card.prototype.toString = function() {
    return this.rank.toString() + " of " + this.suit.toString();
};
// Compare the value of two cards as you would in poker
Card.prototype.compareTo = function(that) {
    if (this.rank < that.rank) return -1;
    if (this.rank > that.rank) return 1;
    return 0;
};

// A function for ordering cards as you would in poker
Card.orderByRank = function(a,b) { return a.compareTo(b); };

// A function for ordering cards as you would in bridge 
Card.orderBySuit = function(a,b) {
    if (a.suit < b.suit) return -1;
    if (a.suit > b.suit) return 1;
    if (a.rank < b.rank) return -1;
    if (a.rank > b.rank) return  1;
    return 0;
};


// Define a class to represent a standard deck of cards
function Deck() {
    var cards = this.cards = [];     // A deck is just an array of cards
    Card.Suit.foreach(function(s) {  // Initialize the array
                          Card.Rank.foreach(function(r) {
                                                cards.push(new Card(s,r));
                                            });
                      });
}
 
// Shuffle method: shuffles cards in place and returns the deck
Deck.prototype.shuffle = function() { 
    // For each element in the array, swap with a randomly chosen lower element
    var deck = this.cards, len = deck.length;
    for(var i = len-1; i > 0; i--) {
        var r = Math.floor(Math.random()*(i+1)), temp;     // Random number
        temp = deck[i], deck[i] = deck[r], deck[r] = temp; // Swap
    }
    return this;
};

// Deal method: returns an array of cards
Deck.prototype.deal = function(n) {  
    if (this.cards.length < n) throw "Out of cards";
    return this.cards.splice(this.cards.length-n, n);
};

// Create a new deck of cards, shuffle it, and deal a bridge hand
var deck = (new Deck()).shuffle();
var hand = deck.deal(13).sort(Card.orderBySuit);

Standard Conversion Methods

Object to Primitive Conversions and Object Methods described important methods used for type
conversion of objects, some of which are invoked automatically by
the JavaScript interpreter when conversion is necessary. You do not
need to implement these methods for every class you write, but they
are important methods, and if you do not implement them for your
classes, it should be a conscious choice not to implement them
rather than mere oversight.

The first, and most important, method is toString(). The purpose of this method is
to return a string representation of an object. JavaScript
automatically invokes this method if you use an object where a
string is expected—as a property name, for example, or with the
+ operator to perform string
concatenation. If you don’t implement this method, your class will
inherit the default implementation from Object.prototype and will convert to the
useless string “[object Object]”. A toString() method might return a
human-readable string suitable for display to end users of your
program. Even if this is not necessary, however, it is often useful
to define toString() for ease of
debugging. The Range and Complex classes in Examples 9-2 and 9-3 have
toString() methods, as do the
enumerated types of Example 9-7. We’ll define
a toString() method for the Set
class of Example 9-6 below.

The toLocaleString() is
closely related to toString(): it
should convert an object to a string in a locale-sensitive way. By
default, objects inherit a toLocaleString() method that simply calls
their toString() method. Some
built-in types have useful toLocaleString() methods that actually
return locale-dependent strings. If you find yourself writing a
toString() method that converts
other objects to strings, you should also define a toLocaleString() method that performs
those conversions by invoking the toLocaleString() method on the objects.
We’ll do this for the Set class below.

The third method is valueOf(). Its job is to convert an object
to a primitive value. The valueOf() method is invoked automatically
when an object is used in a numeric context, with arithmetic
operators (other than +) and with
the relational operators, for example. Most objects do not have a
reasonable primitive representation and do not define this method.
The enumerated types in Example 9-7
demonstrate a case in which the valueOf() method is important,
however.

The fourth method is toJSON(), which is invoked automatically
by JSON.stringify(). The JSON
format is intended for serialization of data structures and can
handle JavaScript primitive values, arrays, and plain objects. It
does not know about classes, and when serializing an object, it
ignores the object’s prototype and constructor. If you call JSON.stringify() on a Range or Complex
object, for example, it returns a string like {"from":1, "to":3} or {"r":1, "i":-1}. If you pass these strings
to JSON.parse(), you’ll obtain a
plain object with properties appropriate for Range and Complex
objects, but which do not inherit the Range and Complex
methods.

This kind of serialization is appropriate for classes like
Range and Complex, but for other classes you may want to write a
toJSON() method to define some
other serialization format. If an object has a toJSON() method, JSON.stringify() does not serialize the
object but instead calls toJSON()
and serializes the value (either primitive or object) that it
returns. Date objects, for example, have a toJSON() method that returns a string
representation of the date. The enumerated types of Example 9-7 do the same: their toJSON() method is the same as their
toString() method. The closest
JSON analog to a set is an array, so we’ll define a toJSON() method below that converts a Set
object to an array of values.

The Set class of Example 9-6 does not define
any of these methods. A set has no primitive representation, so it
doesn’t make sense to define a valueOf() method, but the class should
probably have toString(),
toLocaleString(), and toJSON() methods. We can do that with code
like the following. Note the use of the extend() function (Example 6-2) to add methods to Set.prototype:

// Add these methods to the Set prototype object.
extend(Set.prototype, {
           // Convert a set to a string
           toString: function() {
               var s = "{", i = 0;
               this.foreach(function(v) { s += ((i++ > 0)?", ":"") + v; });
               return s + "}";
           },
           // Like toString, but call toLocaleString on all values
           toLocaleString : function() {
               var s = "{", i = 0;
               this.foreach(function(v) {
                                if (i++ > 0) s += ", ";
                                if (v == null) s += v; // null & undefined
                                else s += v.toLocaleString(); // all others
                            });
               return s + "}";
           },
           // Convert a set to an array of values
           toArray: function() {
               var a = [];
               this.foreach(function(v) { a.push(v); });
               return a;
           }
       });

// Treat sets like arrays for the purposes of JSON stringification.
Set.prototype.toJSON = Set.prototype.toArray;

Comparison Methods

JavaScript equality operators compare objects by reference,
not by value. That is, given two object references, they look to see
if both references are to the same object. They do not check to see
if two different objects have the same property names and values. It
is often useful to be able to compare two distinct objects for
equality or even for relative order (as the < and > operators do). If you define a class
and want to be able to compare instances of that class, you should
define appropriate methods to perform those comparisons.

The Java programming language uses methods for object
comparison, and adopting the Java conventions is a common and useful
thing to do in JavaScript. To enable instances of your class to be
tested for equality, define an instance method named equals(). It should take a single argument
and return true if that argument
is equal to the object it is invoked on. Of course it is up to you
to decide what “equal” means in the context of your own class. For
simple classes you can often simply compare the constructor properties to ensure that the
two objects are of the same type and then compare the instance
properties of the two objects to ensure that they have the same
values. The Complex class in Example 9-3 has an
equals() method of this sort, and
we can easily write a similar one for the Range class:

// The Range class overwrote its constructor property. So add it now.
Range.prototype.constructor = Range;

// A Range is not equal to any nonrange.  
// Two ranges are equal if and only if their endpoints are equal.
Range.prototype.equals = function(that) {
    if (that == null) return false;               // Reject null and undefined
    if (that.constructor !== Range) return false; // Reject non-ranges
    // Now return true if and only if the two endpoints are equal.
    return this.from == that.from && this.to == that.to;
}

Defining an equals() method
for our Set class is somewhat trickier. We can’t just compare the
values property of two sets but
must perform a deeper comparison:

Set.prototype.equals = function(that) {
    // Shortcut for trivial case
    if (this === that) return true;  

    // If the that object is not a set, it is not equal to this one.
    // We use instanceof to allow any subclass of Set.
    // We could relax this test if we wanted true duck-typing.
    // Or we could strengthen it to check this.constructor == that.constructor
    // Note that instanceof properly rejects null and undefined values
    if (!(that instanceof Set)) return false;

    // If two sets don't have the same size, they're not equal
    if (this.size() != that.size()) return false;

    // Now check whether every element in this is also in that.
    // Use an exception to break out of the foreach if the sets are not equal.
    try {
        this.foreach(function(v) { if (!that.contains(v)) throw false; });
        return true;                    // All elements matched: sets are equal.
    } catch (x) {
        if (x === false) return false;  // An element in this is not in that.
        throw x;                        // Some other exception: rethrow it.
    }
};

It is sometimes useful to compare objects according to some
ordering. That is, for some classes, it is possible to say that one
instance is “less than” or “greater than” another instance. You
might order Range object based on the value of their lower bound,
for example. Enumerated types could be ordered alphabetically by
name, or numerically by the associated value (assuming the
associated value is a number). Set objects, on the other hand, do
not really have a natural ordering.

If you try to use objects with JavaScript’s relation
operators, such as < and
<=, JavaScript first calls the
valueOf() method of the objects
and, if this method returns a primitive value, compares those
values. The enumerated types returned by the enumeration() method of Example 9-7 have a valueOf() method and can be meaningfully
compared using the relational operators. Most classes do not have a
valueOf() method, however. To
compare objects of these types according to an explicitly defined
ordering of your own choosing, you can (again, following Java
convention) define a method named compareTo().

The compareTo() method
should accept a single argument and compare it to the object on
which the method is invoked. If the this object is less than the argument,
compare To() should return a value less than
zero. If the this object is
greater than the argument object, the method should return a value
greater than zero. And if the two objects are equal, the method
should return zero. These conventions about the return value are
important, and they allow you to substitute the following
expressions for relational and equality operators:

Replace this With this
a < b a.compareTo(b) < 0
a <= b a.compareTo(b) <= 0
a > b a.compareTo(b) > 0
a >= b a.compareTo(b) >= 0
a == b a.compareTo(b) == 0
a != b a.compareTo(b) != 0

The Card class of Example 9-8 defines a
compareTo() method of this kind,
and we can write a similar method for the Range class to order
ranges by their lower bound:

Range.prototype.compareTo = function(that) {
    return this.from - that.from;
};

Notice that the subtraction performed by this method correctly
returns a value less than zero, equal to zero, or greater than zero,
according to the relative order of the two Ranges. Because the
Card.Rank enumeration in Example 9-8 has a valueOf() method, we could have used this
same idiomatic trick in the compareTo() method of the Card
class.

The equals() methods above
perform type checking on their argument and return false to indicate inequality if the
argument is of the wrong type. The compareTo() method does not have any
return value that indicates “those two values are not comparable,”
so a compareTo() method that does
type checking should typically throw an error when passed an
argument of the wrong type.

Notice that the compareTo()
method we defined for the Range class above returns 0 when two
ranges have the same lower bound. This means that as far as compareTo() is concerned, any two ranges
that start at the same spot are equal. This definition of equality
is inconsistent with the definition used by the equals() method, which requires both
endpoints to match. Inconsistent notions of equality can be a
pernicious source of bugs, and it is best to make your equals() and compareTo() methods consistent. Here is a
revised compareTo() method for
the Range class. It is consistent with equals() and also throws an error if
called with an incomparable value:

// Order ranges by lower bound, or upper bound if the lower bounds are equal.
// Throws an error if passed a non-Range value.
// Returns 0 if and only if this.equals(that).
Range.prototype.compareTo = function(that) {
    if (!(that instanceof Range))
        throw new Error("Can't compare a Range with " + that);
    var diff = this.from - that.from;         // Compare lower bounds
    if (diff == 0) diff = this.to - that.to;  // If equal, compare upper bounds
    return diff;
};

One reason to define a compareTo() method for a class is so that
arrays of instances of that class can be sorted. The Array.sort() method accepts as an optional
argument a comparison function that uses the same return-value
conventions as the compareTo()
method. Given the compareTo()
method shown above, it is easy to sort an array of Range objects
with code like this:

ranges.sort(function(a,b) { return a.compareTo(b); });

Sorting is important enough that you should consider defining
this kind of two-argument
comparison function as a class method for any class for which you
define a compareTo() instance
method. One can easily be defined in terms of the other. For
example:

Range.byLowerBound = function(a,b) { return a.compareTo(b); };

With a method like this defined, sorting becomes
simpler:

ranges.sort(Range.byLowerBound);

Some classes can be ordered in more than one way. The Card
class, for example, defines one class method that orders cards by
suit and another that orders them by rank.

Borrowing Methods

There is nothing special about methods in JavaScript: they are
simply functions assigned to object properties and invoked “through”
or “on” an object. A single function can be assigned to two
properties, and it then serves as two methods. We did this for our
Set class, for example, when we copied the toArray() method and made it do dual-duty
as a toJSON() method as
well.

A single function can even be used as a method of more than
one class. Most of the built-in methods of the Array class, for
example, are defined generically, and if you define a class whose
instances are array-like objects, you can copy functions from
Array.prototype to the prototype
object of your class. If you view JavaScript through the lens of
classical object-oriented languages, the use of methods of one class
as methods of another class can be thought of as a form of multiple
inheritance. JavaScript is not a classical object-oriented language,
however, and I prefer to describe this kind of method reuse using
the informal term borrowing.

It is not only Array methods that can be borrowed: we can
write our own generic methods. Example 9-9
defines generic toString() and
equals() methods that are
suitable for use by simple classes like our Range, Complex, and Card
classes. If the Range class did not have an equals() method, we could
borrow the generic equals() like this:

Range.prototype.equals = generic.equals;

Note that the generic.equals() method does only a
shallow comparison, and it is not suitable for use with classes
whose instance properties refer to objects with their own equals() methods. Also notice that this
method includes special case code to handle the property added to
objects when they are inserted into a Set (Example 9-6).

Example 9-9. Generic methods for borrowing

var generic = {
    // Returns a string that includes the name of the constructor function
    // if available and the names and values of all noninherited, nonfunction
    // properties.
    toString: function() {
        var s = '[';
        // If the object has a constructor and the constructor has a name,
        // use that class name as part of the returned string.  Note that
        // the name property of functions is nonstandard and not supported
        // everywhere.
        if (this.constructor && this.constructor.name)
            s += this.constructor.name + ": ";

        // Now enumerate all noninherited, nonfunction properties
        var n = 0;
        for(var name in this) {
            if (!this.hasOwnProperty(name)) continue;   // skip inherited props
            var value = this[name];
            if (typeof value === "function") continue;  // skip methods
            if (n++) s += ", ";
            s += name + '=' + value;
        }
        return s + ']';
    },

    // Tests for equality by comparing the constructors and instance properties
    // of this and that.  Only works for classes whose instance properties are
    // primitive values that can be compared with ===.
    // As a special case, ignore the special property added by the Set class.
    equals: function(that) {
        if (that == null) return false;
        if (this.constructor !== that.constructor) return false;
        for(var name in this) {
            if (name === "|**objectid**|") continue;     // skip special prop.
            if (!this.hasOwnProperty(name)) continue;    // skip inherited 
            if (this[name] !== that[name]) return false; // compare values
        }
        return true;  // If all properties matched, objects are equal.
    }
};

Private State

In classical object-oriented programming, it is often a goal
to encapsulate or hide the state of an object within the object,
allowing access to that state only through the methods of the
object, and now allowing the important state variables to be read or
written directly. To achieve this goal, languages like Java allow
the declaration of “private” instance fields of a class that are
only accessible to the instance method of the class and cannot be
seen outside of the class.

We can approximate private instance fields using variables (or
arguments) captured in the closure of the constructor invocation
that creates an instance. To do this, we define functions inside the
constructor (so they have access to the constructor’s arguments and
variables) and assign those functions to properties of the newly
created object. Example 9-10 shows how we can do
this to create an encapsulated version of our Range class. Instead
of having from and to properties that give the endpoints of
the range, instances of this new version of the class have from and to methods that return the endpoints of
the range. These from() and
to() methods are defined on the
individual Range object and are not inherited from the prototype.
The other Range methods are defined on the prototype as usual, but
modified to call the from() and
to() methods rather than read the
endpoints directly from properties.

Example 9-10. A Range class with weakly encapsulated endpoints

function Range(from, to) {
    // Don't store the endpoints as properties of this object. Instead
    // define accessor functions that return the endpoint values.
    // These values are stored in the closure.
    this.from = function() { return from; };
    this.to = function() { return to; };
}

// The methods on the prototype can't see the endpoints directly: they have
// to invoke the accessor methods just like everyone else.
Range.prototype = {
    constructor: Range,
    includes: function(x) { return this.from() <= x && x <= this.to(); },
    foreach: function(f) {
        for(var x=Math.ceil(this.from()), max=this.to(); x <= max; x++) f(x);
    },
    toString: function() { return "(" + this.from() + "..." + this.to() + ")"; }
};

This new Range class defines methods for querying the
endpoints of a range, but no methods or properties for setting those
endpoints. This gives instances of this class a kind of
immutability: if used correctly, the endpoints
of a Range object will not change after it has been created. Unless
we use ECMAScript 5 features (see Encapsulating Object State),
however, the from and to properties are still writable, and
Range objects aren’t really immutable at all:

var r = new Range(1,5);             // An "immutable" range
r.from = function() { return 0; };  // Mutate by replacing the method

Keep in mind that there is an overhead to this encapsulation
technique. A class that uses a closure to encapsulate its state will
almost certainly be slower and larger than the equivalent class with
unencapsulated state variables.

Constructor Overloading and Factory Methods

Sometimes we want to allow objects to be initialized in more
than one way. We might want to create a Complex object initialized
with a radius and an angle (polar coordinates) instead of real and
imaginary components, for example, or we might want to create a Set
whose members are the elements of an array rather than the arguments
passed to the constructor.

One way to do this is to overload the
constructor and have it perform different kinds of initialization
depending on the arguments it is passed. Here is an overloaded
version of the Set constructor, for example:

function Set() {          
    this.values = {};     // The properties of this object hold the set
    this.n = 0;           // How many values are in the set

    // If passed a single array-like object, add its elements to the set
    // Otherwise, add all arguments to the set
    if (arguments.length == 1 && isArrayLike(arguments[0])) 
        this.add.apply(this, arguments[0]);
    else if (arguments.length > 0)
        this.add.apply(this, arguments);
}

Defining the Set()
constructor this way allows us to explicitly list set members in the
constructor call or to pass an array of members to the constructor.
The constructor has an unfortunate ambiguity, however: we cannot use
it to create a set that has an array as its sole member. (To do
that, we’d have to create an empty set and then call the add() method explicitly.)

In the case of complex numbers initialized to polar
coordinates, constructor overloading really isn’t viable. Both
representations of complex numbers involve two floating-point
numbers and, unless we add a third argument to the constructor,
there is no way for the constructor to examine its arguments and
determine which representation is desired. Instead, we can write a
factory method—a class method that returns an instance of the class.
Here is a factory method for returning a Complex object initialized
using polar coordinates:

Complex.polar = function(r, theta) {
    return new Complex(r*Math.cos(theta), r*Math.sin(theta));
};

And here is a factory method for initializing a Set from an
array:

Set.fromArray = function(a) {
    s = new Set();      // Create a new empty set
    s.add.apply(s, a);  // Pass elements of array a to the add method
    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 possible 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 The bind() Method.


[15] This example is based on a Java example by Joshua Bloch,
available at http://jcp.org/aboutJava/communityprocess/jsr/tiger/enum.html.

Comments are closed.

loading...