[ Team LiB ] |
3.12 Customizing an Object's PrototypeNN 4, IE 4 3.12.1 ProblemYou want add a property or method to objects that have already been created or are about to be created. 3.12.2 SolutionTo add a property or method to a group of objects built from the same constructor, assign the property or method name and its default value to the prototype property of the object. To demonstrate this concept, we'll start with the coworker object constructor from Recipe 3.8 and create four instances of this object, all stored in an array: function coworker(name, age) { this.name = name; this.age = age || 0; this.show = showAll; } var employeeDB = new Array( ); employeeDB[employeeDB.length] = new coworker("Alice", 23); employeeDB[employeeDB.length] = new coworker("Fred", 32); employeeDB[employeeDB.length] = new coworker("Jean", 28); employeeDB[employeeDB.length] = new coworker("Steve", 24); Each object has two properties and one method assigned to it. Each object's property values are private to that particular object instance. And, although each object shares a method name (and the same function code for that method), when the method executes, it does so within the private context of the single object's instance (e.g., the this keyword in the method code refers to the object instance only). Before or after the object instances exist, you can add a property that belongs to the prototype—an abstract object that represents the "mold" from which the object instances are made. When you assign a property and value to the prototype of the constructor, all object instances—including those that have already been created—gain this new property and value. For example, we can add a property to the coworker constructor that provides employment status information. The default value at the time the prototype property is assigned is the string "on duty": coworker.prototype.status = "on duty"; Each object in the employeeDB array immediately inherits the status property, which is read via the following reference (for any item in the array indexed with integer i): employeeDB[i].status Here is where things get interesting. If you modify the value of the status property of an instance of the object, the value is private to that instance only (akin to overriding a property in a subclass in other languages). All other objects continue to share the default prototype property value. Therefore, if you execute the following statement: employeeDB[2].status = "on sick leave"; the value of all of the other object instances (and any new objects you create via the coworker constructor) show their status to be "on duty." Overridden property values are durable. If, after the above modifications, you change the value of the prototype property, the private property values assigned individually do not reflect the prototype change. For example, changing the prototype status property to reflect the company-wide vacation period is accomplished as follows: coworker.prototype.status = "on vacation"; But the value of employeeDB[2].status continues to be "on sick leave" because the local value was explicitly overridden. From any object reference that inherits a prototype property, you can reach the prototype's value, even if that value has been overridden by the object instance. The object's constructor property points to the constructor function that maintains information about the prototype. For example, the following statement tests the equality of the local status property against the prototype status property: if (employeeDB[2].status != employeeDB[2].constructor.prototype.status) { // the two values aren't the same, so the local value has been overridden } Referencing the constructor's prototype is the JavaScript equivalent of calling super in some truly object-oriented languages. 3.12.3 DiscussionThe following discussion assumes you have experience with, or working knowledge of, object-oriented programming concepts in languages such as Java. Even if you don't, feel free to read along to witness some of the advanced intricacies and possibilities with the JavaScript language. All objects accessible by JavaScript—custom objects, global language objects, and DOM objects (in Netscape 6 and later)—are subject to prototype inheritance. Whenever a statement includes a reference to an object's property (or method), the JavaScript interpreter follows a prototype inheritance chain to find the value (if it exists along the chain) that currently applies. The rules that the interpreter follows are relatively simple:
When a prototype inheritance chain consists of two or more objects, you can also use scripts to access points higher up the chain. For example, an Array constructor inherits from the basic Object constructor. In other words, it is conceivable that a prototype property influencing an instance of an array could be defined for the Object or Array constructor prototype. As shown in the Solution, a reference to the Array's prototype property is: myArray.constructor.prototype.propertyName To reach one level higher, access the (Object) constructor of the (Array) constructor: myArray.constructor.constructor.prototype.propertyName Navigator 4 and later implement a proprietary shortcut syntax for these kinds of upward prototype traversals: the _ _proto_ _ property (with a double underscore before and after the word). I mention it here in case you encounter this syntax in further research about simulating object-oriented techniques in JavaScript. The shortcut equivalents to these references are: myArray._ _proto_ _.propertyName myArray._ _proto_ _._ _proto_ _.propertyName There is no shortcut equivalent in other browsers. It is possible in JavaScript to simulate what some programming languages describe as an interface or implements construction—a way of empowering one object with the properties and methods of another object without creating a subclass of the shared object. The approach to this implementation is not particularly intuitive, but it works nicely once you have it set up. To demonstrate, we'll start with the now familiar coworker object, which contains basic information about a person. In creating another object for a project team members, we find that the coworker object already contains some properties that we'd like to reuse in the project team members object. We'll use two object constructors: one for coworker objects and one for project team members: // coworker object constructor function coworker(name, age) { this.name = name; this.age = age; this.show = showAll; } // teamMember object constructor function teamMember(name, age, projects, hours) { this.projects = projects; this.hours = hours; this.member = coworker; this.member(name, age); } Notice in the teamMember constructor function that the coworker constructor function is assigned to this.member. In other words, invoking the member( ) method of a teamMember object creates a new coworker object. And, in fact, the next statement of the teamMember constructor function invokes the member( ) method, passing two of the incoming parameters to the coworker( ) function. Creating a series of teamMember objects takes the following form: var projectTeams = new Array( ); projectTeams[projectTeams.length] = new teamMember("Alice", 23, ["Gizmo"], 240); projectTeams[projectTeams.length] = new teamMember("Fred", 32, ["Gizmo","Widget"], 325); projectTeams[projectTeams.length] = new teamMember("Jean", 28, ["Gizmo"], 200); projectTeams[projectTeams.length] = new teamMember("Steve", 23, ["Widget"], 190); The result of blending these two constructor functions is that when you create a teamMember object, it has four properties (projects, hours, name, age) and two methods (showAll( ) and member( )). Your scripts wouldn't have much reason to invoke the member( ) method because it's used internally by the teamMember( ) function. But all other pieces of the teamMember object are readily accessible and meaningful to your scripts. An unusual side effect to the connection between these nested objects is that the teamMember objects do not have the coworker constructor in their prototype chain. Therefore, if you assign a property and value to the prototype of the coworker constructor, none of the teamMember objects gain that property. There is, however, a way to place the coworker constructor in the prototype chain of the teamMember object: assign a blank coworker object to the prototype of the teamMember constructor: teamMember.prototype = new coworker( ); You must do this before creating instances of the teamMember object, but you can hold off on assigning specific coworker object prototype properties or methods until later. Then you can do something like the following: coworker.prototype.status = "on duty"; After this statement runs, all instances of teamMember have the additional status property with the default setting. And, just like any prototype property or method, you can override the private value for a single instance without disturbing the default values of the other objects. In Netscape 6 and later, many of these language capabilities add potentially enormous power to the DOM you use every day. These browsers give scripters access to the constructors of every type of DOM object, thus allowing you to add prototype properties and methods to any class of DOM object. For example, if you wish to empower all table elements with a method that removes all rows of nested tbody elements, first define the function that acts as the method, and then assign the function to a prototype method name: function clearTbody( ) { var tbodies = this.getElementsByTagName("tbody"); for (var i = 0; i < tbodies.length; i++) { while (tbodies[i].rows.length > 0) { tbodies[i].deleteRow(0); } } } HTMLTableElement.prototype.clear = clearTbody; Thereafter, you can invoke the clear( ) method of any table element object to let it remove all of its rows: document.getElementById("myTable").clear( ); Given the fact that Mozilla-based browsers expose every W3C DOM object type to scripts, just like the HTMLTableElement, you may get all kinds of wild ideas about extending the properties or methods of all HTML elements or text nodes. Go crazy! |
[ Team LiB ] |