The latter half of Lesson 8 taught you how to create your own data types by creating a constructor function in conjunction with the this
variable to create properties and methods. We'll pick up with the Point data type; its code is repeated here for your convenience:
function Point(x, y) { this.x = x; this.y = y; this.getDistance = function(point) { var x = Math.pow(point.x - this.x, 2); var y = Math.pow(point.y - this.y, 2); return Math.sqrt(x + y); }; }
This implementation has a slight flaw. When calling the constructor with the new
keyword, JavaScript creates a Point object, and in doing so it has to recreate everything within the constructor every time it's called. That is, the x
and y
properties, and a new Function object to serve as the getDistance()
method. In JavaScript, functions are objects, too — complete with their own properties and methods.
In an object-oriented environment, creating a new object should also create its properties. Properties, and more importantly their values, are specific to each instance of a data type, and they should rarely be shared with all other instances of a particular data type. For example, a Point object's x
and y
properties contain values that are independent from another Point object's x
and y
properties. Changing one Point's x
property should not affect another Point object's x
property. So a Point's x
and y
properties should be created when the Point object is created.
A Point object's methods, however, are another matter. From a developer's standpoint, you want a Point object to behave like every other Point object in an application. A change made to a Point object's behavior should be reflected in all other Point objects; otherwise you could not depend on any object to behave how you need it to behave.
In JavaScript, an instance of the Function data type is created every time a function is defined or declared. That means that with every call to the Point()
constructor, JavaScript creates a new function instance for the getDistance()
method. JavaScript shouldn't have to create new behavior with every new object that is created; it should use an already existing behavior and share that behavior with all objects of the same type. This is achieved by means of the prototype
property.
Every Function object has a prototype
property. It is an object that contains properties and methods that are shared among all instances of a particular data type. That means that a change made to any property or method of a particular data type's prototype
object is seen by all instances of that data type. The prototype
object is very much a prototype for every object created with a constructor; the properties and methods defined on a data type's prototype
become members of an object.
That being said, properties generally should be created only in the constructor, not the prototype
object, because properties are typically pieces of data that are unique to each instance of a data type. On the other hand, the shared nature of the prototype
object makes it an ideal place to create methods.
To access a Function object's prototype
object, use the function's name, followed by a dot and the word prototype
. The following code rewrites the Point data type to include the use of the prototype
property:
function Point(x, y) { this.x = x; this.y = y; } Point.prototype.getDistance = function(point) { var x = Math.pow(point.x - this.x, 2); var y = Math.pow(point.y - this.y, 2); return Math.sqrt(x + y); };
Here, the getDistance()
method is moved out of the constructor and added to the Point.prototype
object. This is advantageous for two reasons:
Only one Function object is created and assigned to getDistance
. Calling the Point()
constructor now does not create a new Function object for getDistance()
.
The getDistance()
method is shared among all Point objects. Changes made to the method are seen by all Point objects.
Even though this method is now created on the prototype, you still access the method from an object in the same way, as shown in this code:
var point1 = new Point(0, 0); var point2 = new Point(1, 1); point1.getDistance(point2);
This works because of the way JavaScript looks up identifiers on objects. When accessing this getDistance()
method, JavaScript first performs a search for the getDistance
identifier on the object itself — looking for an identifier with the same name. If JavaScript finds a match, it returns the match's value (remember: Functions can be values). However, if it does not find a match, JavaScript searches for the getDistance
identifier in the data type's prototype
object and returns either the value if a match is found or undefined
if one could not be found.
Because of this behavior, it's possible to override methods defined in the prototype
object. To do this, simply assign a value to the getDistance
identifier, like this:
var point1 = new Point(0, 0); var point2 = new Point(1, 1);point1.getDistance = function(point) {
alert("This method no longer works.");
};
point1.getDistance(point2); // message in alert window
Look at the bold portion of this code. Here, a new function object is created and assigned to the getDistance
member on the object. You might think that this code alters the getDistance()
method on the prototype, but it doesn't. The Point.prototype.getDistance()
method can only be modified by directly accessing it through the prototype
object (that is Point.prototype.getDistance =
new value
). This code actually creates a new getDistance()
method on the point1
object itself, essentially overriding the getDistance()
method defined on the prototype
.
So now when the getDistance()
method on point1
is accessed, JavaScript searches for the getDistance
identifier, finds a match on the point1
object, and returns the newer function. The original getDistance()
method on the prototype
object is still intact, and the following code demonstrates this:
point2.getDistance(point1); // returns 2
Here, the code calls the point2.getDistance()
method. The point2
object does not actually have a getDistance
member, so JavaScript pulls the value from Point.prototype.getDistance
— the function that performs the distance calculation.
Be wary of overriding methods in this manner. The whole idea behind using the prototype
object is that members, usually methods, don't have to be created for each object. Besides, overriding a method like this can cause undesired results.
It is worth reiterating that properties typically don't belong on the prototype. Properties generally should be created inside the constructor.
All objects in JavaScript inherit from the Object data type. Inheritance is an object-oriented concept not too dissimilar from the idea or definition probably running through your head. It's not the type of inheritance by which one gets an heirloom from a family member, or a specific genetic trait, like eye or hair color. It's the type of inheritance by which a sedan has the properties and behaviors of an automobile.
In object-oriented programming, inheritance is all about reusing code. You have a base data type (called the base type, super type, or parent type), and you extend that data type by creating a new data type (called a sub-type or child type) that inherits properties and methods from the base type while also providing new functionality specific to the child-type.
Let's say you've been commissioned to write a portion of an application that keeps track of all employees within a given company. The portion you are in charge of is to write a data type that represents an individual employee. The application needs to know the person's name and title. You incorporated a modified version of your Person data type created in Lesson 8, and another developer on your team is already using it for the portion of the app he is responsible for. The modified Person type looks like this:
function Person(firstName, lastName) { this.firstName = firstName; this.lastName = lastName; } Person.prototype.getFullName = function() { return this.firstName + " " + this.lastName; };
It's a simple data type. It has properties for the person's first and last name, as well as a method to concatenate the two into the person's full name. A lot of work is already done with this data type. Wouldn't it be great if you could use it without altering it (otherwise, your coworker's code may break), while still adding the functionality you need? Inheritance allows you to do so.
There are a number of inheritance patterns in JavaScript, each with its pros and cons. This book teaches you just one pattern.
The first step is to create a new data type that will inherit from the Person data type. This new type is called Employee, and the constructor function has three parameters: the employee's first and last name, and his or her position.
function Employee(firstName, lastName, position) { this.position = position; }
This constructor function creates a position
property and assigns it the value passed to the argument of the same name, but nothing is done with the firstName
and lastName
parameters. As it stands right now, Employee objects have only one property. So Employee needs some way of gaining the firstName
and lastName
properties of the Person type. This is where the call()
method of Function objects comes into play.
The call()
method allows you to call a function as if it was a method of a specific object. In other words, it calls a method with a specified value for the this
variable. The idea is probably easier to understand with some code. The following bolded code adds a new line to the Employee()
constructor function:
function Employee(firstName, lastName, position) {
Person.call(this, firstName, lastName);
this.position = position;
}
So what happens here is that the value of this
(the Employee object being created) is passed to the call()
method, which gives the this
variable in Person()
the value of the Employee object being created. So, essentially, Person()
executes as if it were Employee()
.
The other arguments passed to the call()
method reflect the parameters of the Person()
function: firstName
and lastName
. The result of this code is that firstName
and lastName
properties are added to Employee objects when they are created. So, Employee has now inherited Person's properties, but it still needs to inherit the getFullName()
method.
If getFullName()
were defined inside the constructor, inheritance would be complete and you'd be done. But the method is defined in Person's prototype, so you'll use something called prototype chaining to inherit Person's prototype. The process of prototype chaining is a simple one: Simply create a new instance of the base-type and assign it to the sub-type's prototype, as in this bolded code:
function Employee(firstName, lastName, position) {
Person.call(this, firstName, lastName);
this.position = position;
}
Employee.prototype = new Person();
Prototype chaining gets its name because it adds another prototype to the search list, much as one would add a new link to a chain. Now when you create an Employee object and call the getFullName()
method, JavaScript first looks at the object for a match. It does not find a match, so it looks at Employee's prototype and doesn't find a match there either. So it looks at Person's prototype, since that is a new "link" in the "chain" of prototypes, finds a match, and executes it. Of course, the more prototypes you add to the chain, the more JavaScript has to search for items in prototypes. So it's a good idea to keep prototype chains relatively small.
You can now create an Employee object and use the firstName
, lastName
, and position
properties, as well as call the getFullName()
method. The following code demonstrates this:
var employee = new Employee("Jeremy", "McPeak", "Author"); alert(employee.firstName); // Jeremy alert(employee.lastName); // McPeak alert(employee.getFullName()); // Jeremy McPeak
There's still more to be done, however, so now it's time to...
In this lesson, you learn how to use prototypes to make your code more efficient by creating fewer objects. You also learn about the Function's call()
method, and how to chain prototypes to make inheriting a data type's prototype possible. In this section you learn how to override a base-type's method in a sub-type.
For this lesson, you need a text editor; any plain text editor will do. For Microsoft Windows users, Notepad is available by default on your system or you can download Microsoft's free Visual Web Developer Express (www.microsoft.com/express/web/
) or Web Matrix (www.asp.net/webmatrix/
). Mac OS X users can use TextMate, which comes as part of OS X, or download a trial for Coda (www.panic.com/coda/
). Linux users can use the built-in VIM.
You also need a modern web browser. Choose any of the following:
Internet Explorer 8+
Google Chrome
Firefox 3.5+
Apple Safari 4+
Opera 10+
Create a subfolder called Lesson09
in the JS24Hour
folder you created in Lesson 1. Store the files you create in this lesson in the Lesson09
folder.
The getFullName()
method needs to include the employee's position. Follow these steps to override Person's getFullName()
method to provide that functionality.
Open your text editor and type the following function:
function Person(firstName, lastName) { this.firstName = firstName; this.lastName = lastName; } Person.prototype.getFullName = function() { return this.firstName + " " + this.lastName; }; function Employee(firstName, lastName, position) { Person.call(this, firstName, lastName); this.position = position;
} Employee.prototype = new Person();
This is the Employee data type from this lesson.
Now add a method called getFullName()
to Employee's prototype object. It is bold in the following code:
// Person omitted for printing function Employee(firstName, lastName, position) { Person.call(this, firstName, lastName); this.position = position; } Employee.prototype = new Person();Employee.prototype.getFullName = function() {
};
The Person data type's getFullName()
method has functionality that we want to use. To save time recoding that portion of the method, use the call()
method to call Person's getFullName()
method and assign the result to a variable. The new code is bold in the following:
// Person omitted for printing
function Employee(firstName, lastName, position) {
Person.call(this, firstName, lastName);
this.position = position;
}
Employee.prototype = new Person();
Employee.prototype.getFullName = function() {
var fullName = Person.prototype.getFullName.call(this);
};
This code calls the Person.prototype.getFullName()
method in the context of the current object (the Employee object). The call()
method has to be used here because getFullName()
for Employee objects now has a new value (the method you're creating right now).
Append the employee's position to the string in fullName
and return it:
function Employee(firstName, lastName, position) {
Person.call(this, firstName, lastName);
this.position = position;
}
Employee.prototype = new Person();
Employee.prototype.getFullName = function() {
var fullName = Person.prototype.getFullName.call(this);
return fullName + ", " + this.position;
};
Once again, add the bold code:
function Employee(firstName, lastName, position) { Person.call(this, firstName, lastName); this.position = position; } Employee.prototype = new Person(); Employee.prototype.getFullName = function() { var fullName = Person.prototype.getFullName.call(this); return fullName + ", " + this.position; };var employee = new Employee("Jeremy", "McPeak", "Author");
alert(employee.getFullName());
Save the file as lesson09_sample01.js
.
Open another instance of your text editor, type the following HTML, and save it as lesson09_sample01.htm
.
<html> <head> <title>Lesson 9: Prototypes and Inheritance</title> </head> <body> <script type="text/javascript" src="lesson09_sample01.js"></script> </body> </html>
Open the HTML file in your browser. An alert window displays the result of calling employee.getFullName()
: a string containing the values passed to the Employee()
constructor.
To get the sample code files you can download Lesson 9 from the book's website at www.wrox.com
.
Please select Lesson 9 on the DVD to view the video that accompanies this lesson.
3.17.176.72