loading...

JavaScript – Java-Style Classes in JavaScript

If you have programmed in Java or a similar strongly-typed
object-oriented language, you may be accustomed to thinking about four
kinds of class members:

Instance fields

These are the per-instance properties or variables that
hold the state of individual objects.

Instance methods

These are methods that are shared by all instances of the
class that are invoked through individual instances.

Class fields

These are properties or variables associated with the
class rather than the instances of the class.

Class methods

These are methods that are associated with the class
rather than with instances.

One way JavaScript differs from Java is that its functions are
values, and there is no hard distinction between methods and fields.
If the value of a property is a function, that property defines a
method; otherwise, it is just an ordinary property or “field.” Despite
this difference, we can simulate each of Java’s four categories of
class members in JavaScript. In JavaScript, there are three different
objects involved in any class definition (see Figure 9-1), and the properties of these three objects
act like different kinds of class members:

Constructor object

As we’ve noted, the constructor function (an object)
defines a name for a JavaScript class. Properties you add to
this constructor object serve as class fields and class methods
(depending on whether the property values are functions or
not).

Prototype object

The properties of this object are inherited by all
instances of the class, and properties whose values are
functions behave like instance methods of the class.

Instance object

Each instance of a class is an object in its own right,
and properties defined directly on an instance are not shared by
any other instances. Nonfunction properties defined on instances
behave as the instance fields of the class.

We can reduce the process of class definition in JavaScript to a
three-step algorithm. First, write a constructor function that sets
instance properties on new objects. Second, define instance methods on
the prototype object of the
constructor. Third, define class fields and class properties on the
constructor itself. We can even implement this algorithm as a simple
defineClass() function. (It uses
the extend() function of Example 6-2 as patched in Example 8-3):

// A simple function for defining simple classes
function defineClass(constructor,  // A function that sets instance properties
                     methods,      // Instance methods: copied to prototype
                     statics)      // Class properties: copied to constructor
{
    if (methods) extend(constructor.prototype, methods);
    if (statics) extend(constructor, statics);
    return constructor;
}

// This is a simple variant of our Range class
var SimpleRange =
    defineClass(function(f,t) { this.f = f; this.t = t; },
                {
                    includes: function(x) { return this.f <= x && x <= this.t;},
                    toString: function() { return this.f + "..." + this.t; }
                },
                { upto: function(t) { return new SimpleRange(0, t); } });

Example 9-3 is a longer class definition. It
creates a class that represents complex numbers and demonstrates how
to simulate Java-style class members using JavaScript. It does this
“manually”—without relying on the defineClass() function above.

Example 9-3. Complex.js: A complex number class

/*
 * Complex.js:
 * This file defines a Complex class to represent complex numbers.
 * Recall that a complex number is the sum of a real number and an
 * imaginary number and that the imaginary number i is the square root of -1.
 */

/*
 * This constructor function defines the instance fields r and i on every
 * instance it creates.  These fields hold the real and imaginary parts of
 * the complex number: they are the state of the object.
 */
function Complex(real, imaginary) {
    if (isNaN(real) || isNaN(imaginary)) // Ensure that both args are numbers.
        throw new TypeError();           // Throw an error if they are not.
    this.r = real;                       // The real part of the complex number.
    this.i = imaginary;                  // The imaginary part of the number.
}

/*
 * The instance methods of a class are defined as function-valued properties
 * of the prototype object.  The methods defined here are inherited by all
 * instances and provide the shared behavior of the class. Note that JavaScript
 * instance methods must use the this keyword to access the instance fields.
 */

// Add a complex number to this one and return the sum in a new object.
Complex.prototype.add = function(that) {
    return new Complex(this.r + that.r, this.i + that.i);
};

// Multiply this complex number by another and return the product.
Complex.prototype.mul = function(that) {
    return new Complex(this.r * that.r - this.i * that.i,
                       this.r * that.i + this.i * that.r);
};

// Return the real magnitude of a complex number. This is defined
// as its distance from the origin (0,0) of the complex plane.
Complex.prototype.mag = function() {
    return Math.sqrt(this.r*this.r + this.i*this.i);
};

// Return a complex number that is the negative of this one.
Complex.prototype.neg = function() { return new Complex(-this.r, -this.i); };

// Convert a Complex object to a string in a useful way.
Complex.prototype.toString = function() {
    return "{" + this.r + "," + this.i + "}";
};

// Test whether this Complex object has the same value as another.
Complex.prototype.equals = function(that) {
    return that != null &&                      // must be defined and non-null
        that.constructor === Complex &&         // and an instance of Complex 
        this.r === that.r && this.i === that.i; // and have the same values.
};

/*
 * Class fields (such as constants) and class methods are defined as 
 * properties of the constructor. Note that class methods do not 
 * generally use the this keyword: they operate only on their arguments.
 */

// Here are some class fields that hold useful predefined complex numbers.
// Their names are uppercase to indicate that they are constants.
// (In ECMAScript 5, we could actually make these properties read-only.)
Complex.ZERO = new Complex(0,0);
Complex.ONE = new Complex(1,0);
Complex.I = new Complex(0,1);

// This class method parses a string in the format returned by the toString
// instance method and returns a Complex object or throws a TypeError.
Complex.parse = function(s) {
    try {          // Assume that the parsing will succeed
        var m = Complex._format.exec(s);  // Regular expression magic
        return new Complex(parseFloat(m[1]), parseFloat(m[2]));
    } catch (x) {  // And throw an exception if it fails
        throw new TypeError("Can't parse '" + s + "' as a complex number.");
    }
};

// A "private" class field used in Complex.parse() above.
// The underscore in its name indicates that it is intended for internal
// use and should not be considered part of the public API of this class.
Complex._format = /^\{([^,]+),([^}]+)\}$/;

With the Complex class of Example 9-3 defined,
we can use the constructor, instance fields, instance methods, class
fields, and class methods with code like this:

var c = new Complex(2,3);     // Create a new object with the constructor
var d = new Complex(c.i,c.r); // Use instance properties of c
c.add(d).toString();          // => "{5,5}": use instance methods
// A more complex expression that uses a class method and field
Complex.parse(c.toString()).  // Convert c to a string and back again,
    add(c.neg()).             // add its negative to it,
    equals(Complex.ZERO)      // and it will always equal zero

Although JavaScript classes can emulate Java-style class
members, there are a number of significant Java features that
JavaScript classes do not support. First, in the instance methods of
Java classes, instance fields can be used as if they were local
variables—there is no need to prefix them with this. JavaScript does not do this, but you
could achieve a similar effect using a with statement (this is not recommended,
however):

Complex.prototype.toString = function() {
    with(this) {
        return "{" + r + "," + i + "}";
    }
};

Java allows fields to be declared final to indicate that they are constants,
and it allows fields and methods to be declared private to specify that they are private to
the class implementation and should not be visible to users of the
class. JavaScript does not have these keywords, and Example 9-3 uses typographical conventions to provide
hints that some properties (whose names are in capital letters) should
not be changed and that others (whose names begin with an underscore)
should not be used outside of the class. We’ll return to both of these
topics later in the chapter: private properties can be emulated using
the local variables of a closure (see Private State)
and constant properties are possible in ECMAScript 5 (see Defining Immutable Classes).

Comments are closed.

loading...