Section 9.2. Prototypes and Inheritance


9.2. Prototypes and Inheritance

Recall from Chapter 8 that a method is a function that is invoked as a property of an object. When a function is invoked in this way, the object through which it is accessed becomes the value of the this keyword. Suppose you'd like to compute the area of the rectangle represented by a Rectangle object. Here is one way to do it:

 function computeAreaOfRectangle(r) { return r.width * r.height; } 

This works, but it is not object-oriented. When using objects, it is better to invoke a method on the object rather than passing the object to a function. Here's how to do that:

 // Create a Rectangle object var r = new Rectangle(8.5, 11); // Add a method to it r.area = function( ) { return this.width * this.height; } // Now invoke the method to compute the area var a = r.area( ); 

Having to add a method to an object before you can invoke it is silly, of course. You can improve the situation by initializing the area property to refer to the area computing function in the constructor. Here is an improved Rectangle( ) constructor:

 function Rectangle(w, h) {     this.width = w;     this.height = h;     this.area = function( ) { return this.width * this.height; } } 

With this new version of the constructor, you can write code like this:

 // How big is a sheet of U.S. Letter paper in square inches? var r = new Rectangle(8.5, 11); var a = r.area( ); 

This solution works better but is still not optimal. Every rectangle created will have three properties. The width and height properties may be different for each rectangle, but the area of every single Rectangle object always refers to the same function (someone might change it, of course, but you usually intend the methods of an object to be constant). It is inefficient to use regular properties for methods that are intended to be shared by all objects of the same class (that is, all objects created with the same constructor).

There is a solution, however. It turns out that every JavaScript object includes an internal reference to another object, known as its prototype object. Any properties of the prototype appear to be properties of an object for which it is the prototype. Another way of saying this is that a JavaScript object inherits properties from its prototype.

In the previous section, I showed that the new operator creates a new, empty object and then invokes a constructor function as a method of that object. This is not the complete story, however. After creating the empty object, new sets the prototype of that object. The prototype of an object is the value of the prototype property of its constructor function. All functions have a prototype property that is automatically created and initialized when the function is defined. The initial value of the prototype property is an object with a single property. This property is named constructor and refers back to the constructor function with which the prototype is associated. (You may recall the constructor property from Chapter 7 ; this is why every object has a constructor property.) Any properties you add to this prototype object will appear to be properties of objects initialized by the constructor.

This is clearer with an example. Here again is the Rectangle( ) constructor:

 // The constructor function initializes those properties that // will be different for each instance. function Rectangle(w, h) {     this.width = w;     this.height = h; } // The prototype object holds methods and other properties that // should be shared by each instance. Rectangle.prototype.area = function( ) { return this.width * this.height; } 

A constructor provides a name for a "class" of objects and initializes properties, such as width and height, that may be different for each instance of the class. The prototype object is associated with the constructor, and each object initialized by the constructor inherits exactly the same set of properties from the prototype. This means that the prototype object is an ideal place for methods and other constant properties.

Note that inheritance occurs automatically as part of the process of looking up a property value. Properties are not copied from the prototype object into new objects; they merely appear as if they were properties of those objects. This has two important implications. First, the use of prototype objects can dramatically decrease the amount of memory required by each object because the object can inherit many of its properties. The second implication is that an object inherits properties even if they are added to its prototype after the object is created. This means that it is possible (though not necessarily a good idea) to add new methods to existing classes.

Inherited properties behave just like regular properties of an object. They are enumerated by the for/in loop and can be tested with the in operator. You can distinguish them only with the Object.hasOwnProperty( ) method:

 var r = new Rectangle(2, 3); r.hasOwnProperty("width");   // true: width is a direct property of r r.hasOwnProperty("area");    // false: area is an inherited property of r "area" in r;                 // true: "area" is a property of r 

9.2.1. Reading and Writing Inherited Properties

Each class has one prototype object, with one set of properties. But there are potentially many instances of a class, each of which inherits those prototype properties. Because one prototype property can be inherited by many objects, JavaScript must enforce a fundamental asymmetry between reading and writing property values. When you read property p of an object o, JavaScript first checks to see if o has a property named p. If it does not, it next checks to see if the prototype object of o has a property named p. This is what makes prototype-based inheritance work.

When you write the value of a property, on the other hand, JavaScript does not use the prototype object. To see why, consider what would happen if it did: suppose you try to set the value of the property o.p when the object o does not have a property named p. Further suppose that JavaScript goes ahead and looks up the property p in the prototype object of o and allows you to set the property of the prototype. Now you have changed the value of p for a whole class of objectsnot at all what you intended.

Therefore, property inheritance occurs only when you read property values, not when you write them. If you set the property p in an object o that inherits that property from its prototype, what happens is that you create a new property p directly in o. Now that o has its own property named p, it no longer inherits the value of p from its prototype. When you read the value of p, JavaScript first looks at the properties of o. Since it finds p defined in o, it doesn't need to search the prototype object and never finds the value of p defined there. We sometimes say that the property p in o "shadows" or "hides" the property p in the prototype object. Prototype inheritance can be a confusing topic. Figure 9-1 illustrates the concepts discussed here.

Figure 9-1. Objects and prototypes


Because prototype properties are shared by all objects of a class, it generally makes sense to use them to define only properties that are the same for all objects within the class. This makes prototypes ideal for defining methods. Other properties with constant values (such as mathematical constants) are also suitable for definition with prototype properties. If your class defines a property with a very commonly used default value, you might define this property and its default value in a prototype object. Then, the few objects that want to deviate from the default value can create their own private, unshared copies of the property and define their own nondefault values.

9.2.2. Extending Built-in Types

It is not only user-defined classes that have prototype objects. Built-in classes, such as String and Date, have prototype objects too, and you can assign values to them. For example, the following code defines a new method that is available for all String objects:

 // Returns true if the last character is c String.prototype.endsWith = function(c) {     return (c == this.charAt(this.length-1)) } 

Having defined the new endsWith( ) method in the String prototype object, you can use it like this:

 var message = "hello world"; message.endsWith('h')  // Returns false message.endsWith('d')  // Returns true 

There is a strong argument against extending built-in types with your own methods; if you do so, you are essentially creating your own custom version of the core JavaScript API. Any other programmers who have to read or maintain your code will likely find it confusing if your code includes methods they have never heard of. Unless you are creating a low-level JavaScript framework that you expect to be adopted by many other programmers, it is probably best to stay away from the prototype objects of the built-in types.

Note that you must never add properties to Object.prototype. Any properties or methods you add are enumerable with a for/in loop, and adding them to Object.prototype makes them visible in every single JavaScript object. An empty object, {}, is expected to have no enumerable properties. Anything added to Object.prototype becomes an enumerable property of the empty object and will likely break code that uses objects as associative arrays.

The technique shown here for extending built-in object types is guaranteed to work only for core JavaScript "native objects." When JavaScript is embedded in some context, such as a web browser or a Java application, it has access to additional "host objects" such as objects that represent web browser document content. These host objects do not typically have constructors and prototype objects, and you usually cannot extend them.

There is one case in which it is safe and useful to extend the prototype of a built-in native class: to add standard methods to a prototype when an old or incompatible JavaScript implementation lacks them. For example, the Function.apply( ) method is missing in Microsoft Internet Explorer 4 and 5. This is a pretty important function, and you may see code like this to replace it:

 // IE 4 & 5 don't implement Function.apply( ). // This workaround is based on code by Aaron Boodman. if (!Function.prototype.apply) {     // Invoke this function as a method of the specified object,     // passing the specified parameters.  We have to use eval( ) to do this     Function.prototype.apply = function(object, parameters) {         var f = this;                // The function to invoke         var o = object || window;    // The object to invoke it on         var args = parameters || []; // The arguments to pass         // Temporarily make the function into a method of o         // To do this we use a property name that is unlikely to exist         o._$_apply_$_ = f;         // We will use eval( ) to invoke the method. To do this we've got         // to write the invocation as a string. First build the argument list.         var stringArgs = [];         for(var i = 0; i < args.length; i++)             stringArgs[i] = "args[" + i + "]";         // Concatenate the argument strings into a comma-separated list.         var arglist = stringArgs.join(",");         // Now build the entire method call string         var methodcall = "o._$_apply_$_(" + arglist + ");";         // Use the eval( ) function to make the methodcall         var result = eval(methodcall);         // Unbind the function from the object         delete o._$_apply_$_;         // And return the result         return result;     }; } 

As another example, consider the new array methods implemented in Firefox 1.5 (see Section 7.7.10.). If you want to use the new Array.map( ) method but also want your code to work on platforms that do not support this method natively, you can use this code for compatibility:

 // Array.map( ) invokes a function f on each element of the array, // returning a new array of the values that result from each function // call. If map( ) is called with two arguments, the function f // is invoked as a method of the second argument. When invoked, f( ) // is passed 3 arguments. The first is the value of the array // element. The second is the index of the array element, and the // third is the array itself. In most cases it needs to use only the // first argument. if (!Array.prototype.map) {     Array.prototype.map = function(f, thisObject) {         var results = [];         for(var len = this.length, i = 0; i < len; i++) {             results.push(f.call(thisObject, this[i], i, this));         }         return results;     } } 




JavaScript. The Definitive Guide
JavaScript: The Definitive Guide
ISBN: 0596101996
EAN: 2147483647
Year: 2004
Pages: 767

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net