8.5 Object-Oriented JavaScriptAlthough JavaScript supports a data type we call an object, it does not have a formal notion of a class. This makes it quite different from classic object-oriented languages such as C++ and Java. The common conception about object-oriented programming languages is that they are strongly typed and support class-based inheritance. By these criteria, it is easy to dismiss JavaScript as not being a true object-oriented language. On the other hand, we've seen that JavaScript makes heavy use of objects and that it has its own type of prototype-based inheritance. JavaScript is a true object-oriented language. It draws inspiration from a number of other (relatively obscure) object-oriented languages that use prototype-based inheritance instead of class-based inheritance. Although JavaScript is not a class-based object-oriented language, it does a good job of simulating the features of class-based languages such as Java and C++. I've been using the term class informally throughout this chapter. This section more formally explores the parallels between JavaScript and true class-based inheritance languages such as Java and C++.[4]
Let's start by defining some basic terminology. An object, as we've already seen, is a data structure that contains various pieces of named data and may also contain various methods to operate on those pieces of data. An object groups related values and methods into a single convenient package, which generally makes programming easier by increasing the modularity and reusability of code. Objects in JavaScript may have any number of properties, and properties may be dynamically added to an object. This is not the case in strictly typed languages such as Java and C++. In those languages, each object has a predefined set of properties,[5] where each property is of a predefined type. When we are using JavaScript objects to simulate object-oriented programming techniques, we generally define in advance the set of properties for each object and the type of data that each property holds.
In Java and C++, a class defines the structure of an object. The class specifies exactly what fields an object contains and what types of data each holds. It also defines the methods that operate on an object. JavaScript does not have a formal notion of a class, but, as we've seen, it approximates classes with its constructors and their prototype objects. In both JavaScript and class-based object-oriented languages, there may be multiple objects of the same class. We often say that an object is an instance of its class. Thus, there may be many instances of any class. Sometimes we use the term instantiate to describe the process of creating an object (i.e., an instance of a class). In Java, it is a common programming convention to name classes with an initial capital letter and to name objects with lowercase letters. This convention helps keep classes and objects distinct from each other in code; it is a useful convention to follow in JavaScript programming as well. In previous sections, for example, we've defined the Circle and Rectangle classes and have created instances of those classes named c and rect. The members of a Java class may be of four basic types: instance properties, instance methods, class properties, and class methods. In the following sections, we'll explore the differences between these types and show how they are simulated in JavaScript. 8.5.1 Instance PropertiesEvery object has its own separate copies of its instance properties. In other words, if there are 10 objects of a given class, there are 10 copies of each instance property. In our Circle class, for example, every Circle object has a property r that specifies the radius of the circle. In this case, r is an instance property. Since each object has its own copy of the instance properties, these properties are accessed through individual objects. If c is an object that is an instance of the Circle class, for example, we refer to its radius as: c.r By default, any object property in JavaScript is an instance property. To truly simulate object-oriented programming, however, we will say that instance properties in JavaScript are those properties that are created and/or initialized in an object by the constructor function. 8.5.2 Instance MethodsAn instance method is much like an instance property, except that it is a method rather than a data value. (In Java, functions and methods are not data, as they are in JavaScript, so this distinction is more clear.) Instance methods are invoked on a particular object, or instance. The area( ) method of our Circle class is an instance method. It is invoked on a Circle object c like this: a = c.area( ); Instance methods use the this keyword to refer to the object or instance on which they are operating. An instance method can be invoked for any instance of a class, but this does not mean that each object contains its own private copy of the method, as it does with instance properties. Instead, each instance method is shared by all instances of a class. In JavaScript, we define an instance method for a class by setting a property in the constructor's prototype object to a function value. This way, all objects created by that constructor share an inherited reference to the function and can invoke it using the method invocation syntax shown earlier. 8.5.3 Class PropertiesA class property in Java is a property that is associated with a class itself, rather than with each instance of a class. No matter how many instances of the class are created, there is only one copy of each class property. Just as instance properties are accessed through an instance of a class, class properties are accessed through the class itself. Number.MAX_VALUE is an example of a class property in JavaScript: the MAX_VALUE property is accessed through the Number class. Because there is only one copy of each class property, class properties are essentially global. What is nice about them, however, is that they are associated with a class and they have a logical niche, a position in the JavaScript namespace, where they are not likely to be overwritten by other properties with the same name. As is probably clear, we simulate a class property in JavaScript simply by defining a property of the constructor function itself. For example, to create a class property Circle.PI to store the mathematical constant pi, we can do the following: Circle.PI = 3.14; Circle is a constructor function, but because JavaScript functions are objects, we can create properties of a function just as we can create properties of any other object. 8.5.4 Class MethodsFinally, we come to class methods. A class method is a method associated with a class rather than with an instance of a class; they are invoked through the class itself, not through a particular instance of the class. The Date.parse( ) method (which you can look up in the core reference section of this book) is a class method. You always invoke it through the Date constructor object, rather than through a particular instance of the Date class. Because class methods are not invoked through a particular object, they cannot meaningfully use the this keyword -- this refers to the object for which an instance method is invoked. Like class properties, class methods are global. Because they do not operate on a particular object, class methods are generally more easily thought of as functions that happen to be invoked through a class. Again, associating these functions with a class gives them a convenient niche in the JavaScript namespace and prevents namespace collisions. To define a class method in JavaScript, we simply make the appropriate function a property of the constructor. 8.5.5 Example: The Circle ClassExample 8-5 is a reimplementation of our Circle class that contains examples of each of these four basic types of members. Example 8-5. Defining instance and class properties and methodsfunction Circle(radius) { // The constructor defines the class itself. // r is an instance property, defined and initialized in the constructor. this.r = radius; } // Circle.PI is a class property--it is a property of the constructor function. Circle.PI = 3.14159; // Here is a function that computes a circle's area. function Circle_area( ) { return Circle.PI * this.r * this.r; } // Here we make the function into an instance method by assigning it // to the prototype object of the constructor. // Note: with JavaScript 1.2, we can use a function literal to // define the function without naming it Circle_area. Circle.prototype.area = Circle_area; // Here's another function. It takes two Circle objects as arguments and // returns the one that is larger (i.e., has the larger radius). function Circle_max(a,b) { if (a.r > b.r) return a; else return b; } // Since this function compares two Circle objects, it doesn't make sense as // an instance method operating on a single Circle object. But we don't want // it to be a standalone function either, so we make it into a class method // by assigning it to the constructor function: Circle.max = Circle_max; // Here is some code that uses each of these fields: var c = new Circle(1.0); // Create an instance of the Circle class c.r = 2.2; // Set the r instance property var a = c.area(); // Invoke the area( ) instance method var x = Math.exp(Circle.PI); // Use the PI class property in our own computation var d = new Circle(1.2); // Create another Circle instance var bigger = Circle.max(c,d); // Use the max( ) class method 8.5.6 Example: Complex NumbersExample 8-6 is another example, somewhat more formal than the last, of defining a class of objects in JavaScript. The code and the comments are worth careful study. Note that this example uses the function literal syntax of JavaScript 1.2. Because it requires this version of the language (or later), it does not bother with the JavaScript 1.1 compatibility technique of invoking the constructor once before assigning to its prototype object. Example 8-6. 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. */ /* * The first step in defining a class is defining the constructor * function of the class. This constructor should initialize any * instance properties of the object. These are the essential * "state variables" that make each instance of the class different. */ function Complex(real, imaginary) { this.x = real; // The real part of the number this.y = imaginary; // The imaginary part of the number } /* * The second step in defining a class is defining its instance * methods (and possibly other properties) in the prototype object * of the constructor. Any properties defined in this object will * be inherited by all instances of the class. Note that instance * methods operate implicitly on the this keyword. For many methods, * no other arguments are needed. */ // Return the magnitude of a complex number. This is defined // as its distance from the origin (0,0) of the complex plane. Complex.prototype.magnitude = function( ) { return Math.sqrt(this.x*this.x + this.y*this.y); }; // Return a complex number that is the negative of this one. Complex.prototype.negative = function( ) { return new Complex(-this.x, -this.y); }; // Convert a Complex object to a string in a useful way. // This is invoked when a Complex object is used as a string. Complex.prototype.toString = function( ) { return "{" + this.x + "," + this.y + "}"; }; // Return the real portion of a complex number. This function // is invoked when a Complex object is treated as a primitive value. Complex.prototype.valueOf = function( ) { return this.x; } /* * The third step in defining a class is to define class methods, * constants, and any needed class properties as properties of the * constructor function itself (instead of as properties of the * prototype object of the constructor). Note that class methods * do not use the this keyword: they operate only on their arguments. */ // Add two complex numbers and return the result. Complex.add = function (a, b) { return new Complex(a.x + b.x, a.y + b.y); }; // Subtract one complex number from another. Complex.subtract = function (a, b) { return new Complex(a.x - b.x, a.y - b.y); }; // Multiply two complex numbers and return the product. Complex.multiply = function(a, b) { return new Complex(a.x * b.x - a.y * b.y, a.x * b.y + a.y * b.x); }; // Here are some useful predefined complex numbers. // They are defined as class properties, where they can be used as // "constants." (Note, though, that they are not actually read-only.) Complex.zero = new Complex(0,0); Complex.one = new Complex(1,0); Complex.i = new Complex(0,1); 8.5.7 Superclasses and SubclassesIn Java, C++, and other class-based object-oriented languages, there is an explicit concept of the class hierarchy. Every class can have a superclass from which it inherits properties and methods. Any class can be extended, or subclassed, so that the resulting subclass inherits its behavior. As we've seen, JavaScript supports prototype inheritance instead of class-based inheritance. Still, JavaScript analogies to the class hierarchy can be drawn. In JavaScript, the Object class is the most generic, and all other classes are specialized versions, or subclasses, of it. Another way to say this is that Object is the superclass of all the built-in classes. All classes inherit a few basic methods (described later in this chapter) from Object. We've learned that objects inherit properties from the prototype object of their constructor. How do they also inherit properties from the Object class? Remember that the prototype object is itself an object; it is created with the Object( ) constructor. This means the prototype object itself inherits properties from Object.prototype! So, an object of class Complex inherits properties from the Complex.prototype object, which itself inherits properties from Object.prototype. Thus, the Complex object inherits properties of both objects. When you look up a property in a Complex object, the object itself is searched first. If the property is not found, the Complex.prototype object is searched next. Finally, if the property is not found in that object, the Object.prototype object is searched. Note that because the Complex prototype object is searched before the Object prototype object, properties of Complex.prototype hide any properties with the same name in Object.prototype. For example, in the class definition shown in Example 8-6, we defined a toString( ) method in the Complex.prototype object. Object.prototype also defines a method with this name, but Complex objects never see it because the definition of toString( ) in Complex.prototype is found first. The classes we've shown in this chapter are all direct subclasses of Object. This is typical of JavaScript programming; there is not usually any need to produce a more complex class hierarchy. When necessary, however, it is possible to subclass any other class. For example, suppose we want to produce a subclass of Complex in order to add some more methods. To do this, we simply have to make sure that the prototype object of the new class is itself an instance of Complex, so that it inherits all the properties of Complex.prototype: // This is the constructor for the subclass. function MoreComplex(real, imaginary) { this.x = real; this.y = imaginary; } // We force its prototype to be a Complex object. This means that // instances of our new class inherit from MoreComplex.prototype, // which inherits from Complex.prototype, which inherits from // Object.prototype. MoreComplex.prototype = new Complex(0,0); // Now add a new method or other new features to this subclass. MoreComplex.prototype.swap = function( ) { var tmp = this.x; this.x = this.y; this.y = tmp; } There is one subtle shortcoming to the subclassing technique shown here. Since we explicitly set MoreComplex.prototype to an object of our own creation, we overwrite the prototype object provided by JavaScript and discard the constructor property we are given. This constructor property, described later in this chapter, is supposed to refer to the constructor function that created the object. A MoreComplex object, however, inherits the constructor property of its superclass, rather than having one of its own. One solution is to set this property explicitly: MoreComplex.prototype.constructor = MoreComplex; Note, however, that in JavaScript 1.1, the constructor property is read-only and cannot be set in this way. |