loading...

JavaScript – Classes and Constructors

Example 9-1 demonstrates one way to define a
JavaScript class. It is not the idiomatic way to do so, however,
because it did not define a constructor. A
constructor is a function designed for the initialization of newly
created objects. Constructors are invoked using the new keyword as described in Constructor Invocation. Constructor invocations using
new automatically create the new
object, so the constructor itself only needs to initialize the state
of that new object. The critical feature of constructor invocations is
that the prototype property of the
constructor is used as the prototype of the new object. This means
that all objects created with the same constructor inherit from the
same object and are therefore members of the same class. Example 9-2 shows how we could alter the range class of
Example 9-1 to use a constructor function instead of a
factory function:

Example 9-2. A Range class using a constructor

// range2.js: Another class representing a range of values.  

// This is a constructor function that initializes new Range objects.
// Note that it does not create or return the object. It just initializes this.
function Range(from, to) {
    // Store the start and end points (state) of this new range object.
    // These are noninherited properties that are unique to this object.
    this.from = from;
    this.to = to;
}

// All Range objects inherit from this object.
// Note that the property name must be "prototype" for this to work.
Range.prototype = {
    // Return true if x is in the range, false otherwise
    // This method works for textual and Date ranges as well as numeric.
    includes: function(x) { return this.from <= x && x <= this.to; },
    // Invoke f once for each integer in the range.
    // This method works only for numeric ranges.
    foreach: function(f) {
        for(var x = Math.ceil(this.from); x <= this.to; x++) f(x);
    },
    // Return a string representation of the range
    toString: function() { return "(" + this.from + "..." + this.to + ")"; }
};

// Here are example uses of a range object
var r = new Range(1,3);   // Create a range object
r.includes(2);            // => true: 2 is in the range
r.foreach(console.log);   // Prints 1 2 3
console.log(r);           // Prints (1...3)

It is worth comparing Example 9-1 and Example 9-2 fairly carefully and noting the differences
between these two techniques for defining classes. First, notice that
we renamed the range() factory
function to Range() when we
converted it to a constructor. This is a very common coding
convention: constructor functions define, in a sense, classes, and
classes have names that begin with capital letters. Regular functions
and methods have names that begin with lowercase letters.

Next, notice that the Range()
constructor is invoked (at the end of the example) with the new keyword while the range() factory function was invoked without
it. Example 9-1 uses regular function invocation
(Function Invocation) to create the new object and
Example 9-2 uses constructor invocation (Constructor Invocation). Because the Range() constructor is invoked with new, it does not have to call inherit() or take any action to create a new
object. The new object is automatically created before the constructor
is called, and it is accessible as the this value. The Range() constructor merely has to initialize
this. Constructors do not even have
to return the newly created object. Constructor invocation
automatically creates a new object, invokes the constructor as a
method of that object, and returns the new object. The fact that
constructor invocation is so different from regular function
invocation is another reason that we give constructors names that
start with capital letters. Constructors are written to be invoked as
constructors, with the new keyword,
and they usually won’t work properly if they are invoked as regular
functions. A naming convention that keeps constructor functions
distinct from regular functions helps programmers to know when to use
new.

Another critical difference between Example 9-1
and Example 9-2 is the way the prototype object is
named. In the first example, the prototype was range.methods. This was a convenient and
descriptive name, but arbitrary. In the second example, the prototype
is Range.prototype, and this name
is mandatory. An invocation of the Range() constructor automatically uses
Range.prototype as the prototype of
the new Range object.

Finally, also note the things that do not change between Example 9-1 and Example 9-2: the range
methods are defined and invoked in the same way for both
classes.

Constructors and Class Identity

As we’ve seen, the prototype object is fundamental to the
identity of a class: two objects are instances of the same class if
and only if they inherit from the same prototype object. The
constructor function that initializes the state of a new object is
not fundamental: two constructor functions may have prototype properties that point to the
same prototype object. Then both constructors can be used to create
instances of the same class.

Even through constructors are not as fundamental as
prototypes, the constructor serves as the public face of a class.
Most obviously, the name of the constructor function is usually
adopted as the name of the class. We say, for example, that the
Range() constructor creates Range
objects. More fundamentally, however, constructors are used with the
instanceof operator when testing
objects for membership in a class. If we have an object r and want to know if it is a Range
object, we can write:

r instanceof Range   // returns true if r inherits from Range.prototype

The instanceof operator
does not actually check whether r
was initialized by the Range
constructor. It checks whether it inherits from Range.prototype. Nevertheless, the
instanceof syntax reinforces the
use of constructors as the public identity of a class. We’ll see the
instanceof operator again later
in this chapter.

The constructor Property

In Example 9-2 we set Range.prototype to a new object that
contained the methods for our class. Although it was convenient to
express those methods as properties of a single object literal, it
was not actually necessary to create a new object. Any JavaScript
function can be used as a
constructor, and constructor invocations need a prototype property. Therefore, every
JavaScript function (except functions returned by the ECMAScript 5
Function.bind() method)
automatically has a prototype
property. The value of this property is an object that has a single
nonenumerable constructor
property. The value of the constructor property is the function
object:

var F = function() {}; // This is a function object. 
var p = F.prototype;   // This is the prototype object associated with it.
var c = p.constructor; // This is the function associated with the prototype.
c === F                // => true: F.prototype.constructor === F for any function

The existence of this predefined prototype object with its
constructor property means that
objects typically inherit a constructor property that refers to their
constructor. Since constructors serve as the public identity of a
class, this constructor property gives the class of an
object:

var o = new F();      // Create an object o of class F
o.constructor === F   // => true: the constructor property specifies the class

Figure 9-1 illustrates this relationship
between the constructor function, its prototype object, the back
reference from the prototype to the constructor, and the instances
created with the constructor.

Figure 9-1. A constructor function, its prototype, and
instances

Notice that Figure 9-1 uses our Range() constructor as an example. In
fact, however, the Range class defined in Example 9-2 overwrites the predefined Range.prototype object with an object of
its own. And the new prototype object it defines does not have a
constructor property. So
instances of the Range class, as defined, do not have a constructor property. We can remedy this
problem by explicitly adding a constructor to the
prototype:

Range.prototype = {
    constructor: Range,  // Explicitly set the constructor back-reference
    includes: function(x) { return this.from <= x && x <= this.to; },
    foreach: function(f) {
        for(var x = Math.ceil(this.from); x <= this.to; x++) f(x);
    },
    toString: function() { return "(" + this.from + "..." + this.to + ")"; }
};

Another common technique is to use the predefined prototype
object with its constructor property, and add methods
to it one at a time:

// Extend the predefined Range.prototype object so we don't overwrite
// the automatically created Range.prototype.constructor property.
Range.prototype.includes = function(x) { return this.from<=x && x<=this.to; };
Range.prototype.foreach = function(f) {
    for(var x = Math.ceil(this.from); x <= this.to; x++) f(x);
};
Range.prototype.toString = function() {
    return "(" + this.from + "..." + this.to + ")";
};

Comments are closed.

loading...