JavaScript – Classes and Types

Recall from Chapter 3 that JavaScript defines
a small set of types: null, undefined, boolean, number, string,
function, and object. The typeof
operator (The typeof Operator) allows us to distinguish among
these types. Often, however, it is useful to treat each class as its
own type and to be able to distinguish objects based on their class.
The built-in objects of core JavaScript (and often the host objects of
client-side JavaScript) can be distinguished on the basis of their
class attribute (The class Attribute) using code like the classof() function of Example 6-4. But when we define our own classes using the
techniques shown in this chapter, the instance objects always have a
class attribute of “Object”, so the classof() function doesn’t help
here.

The subsections that follow explain three techniques for
determining the class of an arbitrary object: the instanceof operator, the constructor property, and the name of the
constructor function. None of these techniques is entirely
satisfactory, however, and the section concludes with a discussion of
duck-typing, a programming philosophy that focuses on what an object
can do (what methods it has) rather than what its class is.

The instanceof operator

The instanceof operator was
described in The instanceof Operator. The left-hand operand
should be the object whose class is being tested, and the right-hand
operand should be a constructor function that names a class. The
expression o instanceof c
evaluates to true if o inherits from c.prototype. The inheritance need not be
direct. If o inherits from an
object that inherits from an object that inherits from c.prototype, the expression will still
evaluate to true.

As noted earlier in this chapter, constructors act as the
public identity of classes, but prototypes are the fundamental
identity. Despite the use of a constructor function with instanceof, this operator is really
testing what an object inherits from, not what constructor was used
to create it.

If you want to test the prototype chain of an object for a
specific prototype object and do not want to use the constructor
function as an intermediary, you can use the is Prototype Of() method. For
example, we could test whether an object r was a member of the range class defined
in Example 9-1 with this code:

range.methods.isPrototypeOf(r);  // range.methods is the prototype object.

One shortcoming of the instanceof operator and the isPrototypeOf() method is that they do not
allow us to query the class of an object, only to test an object
against a class we specify. A more serious shortcoming arises in
client-side JavaScript where a web application uses more than one
window or frame. Each window or frame is a distinct execution
context, and each has its own global object and its own set of
constructor functions. Two arrays created in two different frames
inherit from two identical but distinct prototype objects, and an
array created in one frame is not instanceof the Array() constructor of another
frame.

The constructor property

Another way to identify the class of an object is to simply
use the constructor property.
Since constructors are the public face of classes, this is a
straightforward approach. For example:

function typeAndValue(x) {
    if (x == null) return "";  // Null and undefined don't have constructors
    switch(x.constructor) {
    case Number:  return "Number: " + x;         // Works for primitive types
    case String:  return "String: '" + x + "'";
    case Date:    return "Date: " + x;           // And for built-in types
    case RegExp:  return "Regexp: " + x;
    case Complex: return "Complex: " + x;        // And for user-defined types
    }
}

Note that the expressions following the case keyword in the code above are
functions. If we were using the typeof operator or extracting the
class attribute of the object, they would be
strings instead.

This technique of using the constructor property is subject to the
same problem as instanceof. It
won’t always work when there are multiple execution contexts (such
as multiple frames in a browser window) that share values. In this
situation, each frame has its own set of constructor functions: the
Array constructor in one frame is
not the same as the Array
constructor in another frame.

Also, JavaScript does not require that every object have a
constructor property: this is a
convention based on the default prototype object created for each
function, but it is easy to accidentally or intentionally omit the
constructor property on the
prototype. The first two classes in this chapter, for example, were
defined in such a way (in Examples
9-1
and 9-2)
that their instances did not have constructor properties.

The Constructor Name

The main problem with using the instanceof operator or the constructor property for determining the
class of an object occurs when there are multiple execution contexts
and thus multiple copies of the constructor functions. These
functions may well be identical, but they are distinct objects and
are therefore not equal to each other.

One possible workaround is to use the name of the constructor
function as the class identifier rather than the function itself.
The Array constructor in one
window is not equal to the Array
constructor in another window, but their names are equal. Some
JavaScript implementations make the name of a function available
through a nonstandard name
property of the function object. For implementations without a
name property, we can convert the
function to a string and extract the name from that. (We did this in
Augmenting Classes when we showed how to add a
getName() method to the Function
class.)

Example 9-4 defines a type() function that returns the type of
an object as a string. It handles primitive values and functions
with the typeof operator. For
objects, it returns either the value of the
class attribute or the name of the constructor.
The type() function uses the
classof() function from Example 6-4 and the Function.getName() method from Augmenting Classes. The code for that function and
method are included here for simplicity.

Example 9-4. A type() function to determine the type of a value

/**
 * Return the type of o as a string:
 *   -If o is null, return "null", if o is NaN, return "nan".
 *   -If typeof returns a value other than "object" return that value.
 *    (Note that some implementations identify regexps as functions.)
 *   -If the class of o is anything other than "Object", return that.
 *   -If o has a constructor and that constructor has a name, return it.
 *   -Otherwise, just return "Object".
 **/
function type(o) {
    var t, c, n;  // type, class, name

    // Special case for the null value:
    if (o === null) return "null";

    // Another special case: NaN is the only value not equal to itself:
    if (o !== o) return "nan";

    // Use typeof for any value other than "object".
    // This identifies any primitive value and also functions.
    if ((t = typeof o) !== "object") return t;

    // Return the class of the object unless it is "Object".
    // This will identify most native objects.
    if ((c = classof(o)) !== "Object") return c;

    // Return the object's constructor name, if it has one
    if (o.constructor && typeof o.constructor === "function" &&
        (n = o.constructor.getName())) return n;

    // We can't determine a more specific type, so return "Object"
    return "Object";
}

// Return the class of an object.
function classof(o) {
    return Object.prototype.toString.call(o).slice(8,-1);
};
    
// Return the name of a function (may be "") or null for nonfunctions
Function.prototype.getName = function() {
    if ("name" in this) return this.name;
    return this.name = this.toString().match(/function\s*([^(]*)\(/)[1];
};

This technique of using the constructor name to identify the
class of an object has one of the same problems as using the
constructor property itself: not
all objects have a constructor
property. Furthermore, not all functions have a name. If we define a
constructor using an unnamed
function definition expression, the getName() method will return an empty
string:

// This constructor has no name
var Complex = function(x,y) { this.r = x; this.i = y; } 
// This constructor does have a name
var Range = function Range(f,t) { this.from = f; this.to = t; }

Duck-Typing

None of the techniques described above for determining the
class of an object are problem-free, at least in client-side
JavaScript. An alternative is to sidestep the issue: instead of
asking “what is the class of this object?” we ask instead, “what can
this object do?” This approach to programming is common in languages
like Python and Ruby and is called duck-typing
after this expression (often attributed to poet James Whitcomb
Riley):

When I see a bird that walks like a duck and swims like a
duck and quacks like a duck, I call that bird a duck.

For JavaScript programmers, this aphorism can be understood to
mean “if an object can walk and swim and quack like a Duck, then we
can treat it as a Duck, even if it does not inherit from the
prototype object of the Duck class.”

The Range class of Example 9-2 serves as an
example. This class was designed with numeric ranges in mind.
Notice, however, that the Range()
constructor does not check its arguments to ensure that they are
numbers. It does use the >
operator on them, however, so it assumes that they are comparable.
Similarly, the includes() method
uses the <= operator but makes
no other assumptions about the endpoints of the range. Because the
class does not enforce a particular type, its includes() method works for any kind of
endpoint that can be compared with the relational operators:

var lowercase = new Range("a", "z");
var thisYear = new Range(new Date(2009, 0, 1), new Date(2010, 0, 1));

The foreach() method of our
Range class doesn’t explicitly test the type of the range endpoints
either, but its use of Math.ceil() and the ++ operator means that it only works with
numeric endpoints.

As another example, recall the discussion of array-like
objects from Array-Like Objects. In many circumstances, we
don’t need to know whether an object is a true instance of the Array
class: it is enough to know that it has a nonnegative integer
length property. The existence of
an integer-valued length is how
arrays walk, we might say, and any object that can walk in this way
can (in many circumstances) be treated as an array.

Keep in mind, however, that the length property of true arrays has special
behavior: when new elements are added, the length is automatically
updated, and when the length is set to a smaller value, the array is
automatically truncated. We might say that this is how arrays swim
and quack. If you are writing code that requires swimming and
quacking, you can’t use an object that only walks like an
array.

The examples of duck-typing presented above involve the
response of objects to the <
operator and the special behavior of the length property. More typically, however,
when we talk about duck-typing, we’re talking about testing whether
an object implements one or more methods. A strongly-typed triathlon() function might require its
argument to be an TriAthlete object. A duck-typed alternative could
be designed to accept any object that has walk(), swim(), and bike() methods. Less frivolously, we might
redesign our Range class so that instead of using the < and ++ operators, it uses the compareTo() and succ() (successor) methods of its endpoint
objects.

One approach to duck-typing is laissez-faire: we simply assume
that our input objects implement the necessary methods and perform
no checking at all. If the assumption is invalid, an error will
occur when our code attempts to invoke a nonexistent method. Another
approach does check the input objects. Rather than check their
class, however, it checks that they implement methods with the
appropriate names. This allows us to reject bad input earlier and
can result in more informative error messages.

Example 9-5 defines a quacks() function (“implements” would be a
better name, but implements is a
reserved word) that can be useful when duck-typing. quacks() tests whether an object (the
first argument) implements the methods specified by the remaining
arguments. For each remaining argument, if the argument is a string,
it checks for a method by that name. If the argument is an object,
it checks whether the first object implements methods with the same
names as the methods of that object. If the argument is a function,
it is assumed to be a constructor, and the function checks whether
the first object implements methods with the same names as the
prototype object.

Example 9-5. A function for duck-type checking

// Return true if o implements the methods specified by the remaining args.
function quacks(o /*, ... */) {
    for(var i = 1; i < arguments.length; i++) {  // for each argument after o
        var arg = arguments[i];
        switch(typeof arg) { // If arg is a:
        case 'string':       // string: check for a method with that name
            if (typeof o[arg] !== "function") return false;
            continue;
        case 'function':     // function: use the prototype object instead
            // If the argument is a function, we use its prototype object
            arg = arg.prototype;
            // fall through to the next case
        case 'object':       // object: check for matching methods
            for(var m in arg) { // For each property of the object
                if (typeof arg[m] !== "function") continue; // skip non-methods
                if (typeof o[m] !== "function") return false;
            }
        }
    }
    
    // If we're still here, then o implements everything
    return true;
}

There are a couple of important things to keep in mind about
this quacks() function. First, it
only tests that an object has one or more function-valued properties
with specified names. The existence of these properties doesn’t tell
us anything about what those functions do or how many and what kind
of arguments they expect. This, however, is the nature of
duck-typing. If you define an API that uses duck-typing rather than
a stronger version of type checking, you are creating a more
flexible API but also entrusting the user of your API with the
responsibility to use the API correctly. The second important point
to note about the quacks()
function is that it doesn’t work with built-in classes. For example,
you can’t write quacks(o, Array)
to test that o has methods with the same names as all Array methods.
This is because the methods of the built-in classes are
nonenumerable and the for/in loop
in quacks() does not see them.
(Note that this can be remedied in ECMAScript 5 with the use of
Object.getOwnPropertyNames().)

Comments are closed.