3.12 Customizing an Object's Prototype
NN 4, IE 4
3.12.1 Problem
You want add a property or
method to objects that have already
been created or are about to be created.
3.12.2 Solution
To 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 Discussion
The 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:
If the object has a private value assigned to that property (as is
the case more than 99 percent of the time), that is the value
returned by the reference.
If no local value exists, the interpreter looks for that property and
value in the constructor prototype for that object.
If no property or value exists in the constructor prototype, the
interpreter follows the prototype chain all the way to the basic
Object object if necessary.
When no property by that name exists in the prototype inheritance
chain, the interpreter indicates an
"undefined" value for that
property.
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!
|