JavaScript Object-Oriented Programming Part 2 Article
In Part 1 of this series, we covered objects, object methods, and object categories. Now, let’s move on.
Arguments
In every function, a private variable — argument
— is automatically created, holding an array of the arguments passed to the function. For example:
function testArg(){
for(i=0;i<arguments.length;i++){
alert("Argument "+i+" is "+arguments[i]);
}
}
As demonstrated in the example above, we can access the set of arguments passed when calling a function with the arguments
variable that exists in the function’s scope. This example shows that we can access all of the arguments in a function without specifying them as parameters when we define the function. This can be particularly useful when we don’t know exactly how many arguments we’re going to pass.
Therefore, we can use:
testArg("PageResource","SitePoint","JavaScriptCity",
"WebSite Abstraction");
…to get an alert of some of my favorite Web development sites.
Complex Example
Now that we have a foundation in object-based programming in JavaScript, let’s build an intricate object-based example, a library. We’ll just keep track of some basic information, such as the book titles, authors, pages, and price. To accomplish this, we’re going to have a Person
object (that represents each author), a Book
object, and a Library
object. First, let’s create the Person()
object constructor:
function Person(lastName, firstName){
this.lastName = lastName;
this.firstName = firstName;
}
And now, let’s create some instances of our Person
object:
var DnnyGdmn = new Person("Goodman","Danny");
var DvdFlngn = new Person("Flanagan","David");
var TmMyrs = new Person("Myers","Tom");
var AlxNkmvsky = new Person("Nakhimovsky","Alexander");
Next, let’s create our Book
object. Its properties will be:
- title
- pages
- price
- author(s)
Lastly, we can have multiple authors for the same book, so we need to be able to accept more than one Person
object as an author. To do this, we’ll create an array to hold each person that wrote the book.
function Book(title, pages, price){
this.title = title;
this.pages = pages;
this.price = price;
this.authors = new Array(arguments.length-3);
for(i=0;i<arguments.length-3;i++){
this.authors[i] = arguments[i+3];
}
}
The first part of that code should seem straightforward; however, the last part may not. So, let’s examine it more closely:
this.authors = new Array(arguments.length-3);
This creates an author
property for our Book
object. The author
property is itself an Array
object. When we call our Book()
constructor, the first three arguments are title, pages
, and price
, respectively, so those arguments that are specified after these are our authors. Therefore, if we pass five arguments, we know that two of those must be authors. So we can create an Array
object with a length of arguments.length-3
.
for(i=0;i<arguments.length-3;i++){
this.authors[i] = arguments[i+3];
}
This code loops through the arguments and assigns them to the Array
object. Now, let’s see how we can create instances of this Book
object:
var JavaNut = new Book("Java Foundation Classes in a
Nutshell", 731, 29.95, DvdFlngn);
var JSTDR = new Book("Javascript: The Definitive Guide (3rd
Edition)", 776, 39.95, DvdFlngn);
var JSBible = new Book("Javascript Bible, 4th Edition",
1200, 49.99, DnnyGdmn);
var DHTMLTDR = new Book("Dynamic Html: The Definitive
Reference", 1073, 44.95, DnnyGdmn);
var JSObj = new Book("JavaScript Objects", 450, 39.99,
TmMyrs, AlxNkmvsky);
Note that we’re passing instances of the Person
object as the last arguments to create the authors
property of the Book
object. A key concept in OOP design (as in Relational Database design) is to avoid repetition within data. Therefore, we create one Person
object for each distinct author. So, even though David Flanagan may write more than one book, we always refer to the same Person
object. Also, if David ever decides to change his first name to "Bebop", we can easily change it for all records, by simply altering the one Person
object that holds this information. In addition, note that instead of passing primitive data types, we could have passed objects. For instance, for the title, we could have passed a String
object, and for the page number, we could have passed a Number
object. However, here, they wouldn’t serve much use, so we used a primitive data type – this fits our needs just perfectly.
Now, let’s move on to perhaps the most difficult object constructor, the Library()
constructor. I’m going to break this one up in to parts:
function Library(){
this.books = new Array(arguments.length);
for(i=0;i<arguments.length;i++){
this.books[i] = arguments[i];
}
The first thing you may notice about this function is that it has no parameters. This is because it only accepts Book
objects, though we have no idea how many. It creates a property of the Library
object, books
, which stores an Array
of Book
objects. Let’s say we wanted to access a book’s first-listed author. We could use:
this.books[bookIndex].authors[0]
We first access the Library’s book
property, which is an Array
object. Then, we access a specific Book
object. From that Book
object, we access its authors
property, which is an array. Lastly, we access a specific Person
object. And from there, we could go on to access that Person
object’s firstName
or lastName
property. Note that bookIndex
is the index of the book we want to access.
Now, that’s the only property that our Library
object will contain. The rest will be methods:
this.totalPrice = function(){
var totalCost = 0;
for(i=0;i<this.books.length;i++){
totalCost += this.books[i].price;
}
return totalCost;
}
This method loops through our books
property, which is an Array
object, takes the price of each Book
object and adds it up, and finally returns the value.
this.averagePrice = new Function("return this.totalPrice
()/this.books.length");
This method takes the total price of all of our books and divides it by the number of books we have, to find out the average price of our books. So, once we create a Library
object, how do we add more books to it? We’re going to have to create another function:
this.addBook = new Function("book", "this.books.push(book)");
This uses the Array
‘s built-in method, push()
. The push()
method adds the value or object passed as an argument to its Array
object, while making sure to change the Array
‘s length
property. Lastly, we’ll create a method to display the names of the authors in our library. This method is rather long, so I’ll split it up:
this.getAuthors = function(){
var toSay = "Your favorite authors are:n";
This creates a method we’ll use to retrieve the list of authors. The toSay
variable will hold the string of what this method returns.
for(i=0;i<this.books.length;i++){
for(j=0; j<this.books[i].authors.length;
j++){
var authName =
this.books[i].authors[j].firstName + " " +
this.books[i].authors[j].lastName;
This portion of the code loops through all the books, and then loops through all the authors of that book, placing their names in the authName
variable.
if(toSay.indexOf(authName)!=-1) continue;
toSay+="nt"+authName;
If this author is already in the toSay
variable, we don’t want to add him again, so we continue to loop through the authors of this book. However, if the author is not yet listed, we can go ahead and add him or her to the toSay
variable.
}
}
return toSay;
}
}
That closes the two for
loops we had open, and returns the toSay
variable. It also closes out the method we’ve been defining, getAuthors()
, as well as closing out the Library()
constructor. Now, let’s put all the code together, and create a new Library
object:
// define our Person() constructor
function Person(lastName, firstName){
this.lastName = lastName;
this.firstName = firstName;
}
// define our Book() constructor
function Book(title, pages, price){
this.title = title;
this.pages = pages;
this.price = price;
this.authors = new Array(arguments.length-3);
for(i=0;i<arguments.length-3;i++){
this.authors[i] = arguments[i+3];
}
}
//define our Library() constructor
function Library(){
this.books = new Array(arguments.length);
for(i=0;i<arguments.length;i++){
this.books[i] = arguments[i];
}
this.totalPrice = function(){
var totalCost = new Number(0);
for(i=0;i<this.books.length;i++){
totalCost += this.books[i].price;
}
return totalCost;
}
this.averagePrice = new Function("return
this.totalPrice()/this.books.length");
this.addBook = new
Function("book","this.books.push(book)");
this.getAuthors = function(){
var toSay = "Your favorite authors are:n";
for i=0;i<this.books.length;i++){
for(j=0;j<this.books[i].authors.length;j++){
var authName =
this.books[i].authors[j].firstName + " " +
this.books[i].authors[j].lastName;
if(toSay.indexOf(authName)!=-
1)continue;
toSay+="nt"+authName;
}
}
return toSay;
}
}
// create some Person objects
DnnyGdmn = new Person("Goodman","Danny");
DvdFlngn = new Person("Flanagan","David");
TmMyrs = new Person("Myers","Tom");
AlxNkmvsky = new Person("Nakhimovsky","Alexander");
// create some Book objects
JavaNut = new Book("Java Foundation Classes in a
Nutshell",731,29.95,DvdFlngn);
JSTDR = new Book("Javascript: The Definitive Guide (3rd
Edition)",776,39.95,DvdFlngn);
JSBible = new Book("Javascript Bible, 4th
Edition",1200,49.99,DnnyGdmn);
DHTMLTDR = new Book("Dynamic Html: The Definitive
Reference",1073,44.95,DnnyGdmn);
JSObj = new Book("JavaScript
Objects",450,39.99,TmMyrs,AlxNkmvsky);
// create a Library object
myLib = new Library(JavaNut,JSTDR,JSBible,DHTMLTDR);
Oops, we left out the JavaScript Objects book. We’d better add it:
myLib.addBook(JSObj);
Now, we can get the information, such as how much our library of books cost, the average price of a book, and the names of the authors that have written the various books we own. And that’s it! We’ve completed a complicated OOP example with JavaScript. You might want to go back over any code that you don’t understand, or feel free to post a question in the Client Side Scripting forum at SitePointForums.com.
Prototype
Every object constructor has a special property, prototype
. This property allows you to add properties/methods to all objects created from that object constructor. Sound confusing? It’s not. Let’s look at some examples:
function Square(){
}
var squareObj = new Square();
Square.prototype.side = 5;
var squareObj2 = new Square();
alert(squareObj.side); // displays 5
alert(squareObj2.side); // displays 5
What this does is add a side property, with an initial value of 5, to all Square
objects, whether they’ve been created, or have yet to be created. The prototype
object (it is, in fact, an object) loads before the object constructor does anything. So, this code:
function Square(){
this.side=5;
}
var squareObj = new Square();
Square.prototype.side = 4;
var squareObj2 = new Square();
alert(squareObj.side); // displays 5
alert(squareObj2.side); // displays 5
returns 5, because everything in the prototype
object loads first (before the Square()
object constructor even runs), so the properties and methods defined in the constructor will override it. So, with the prototype
property, you can’t override any properties or methods defined in an object’s constructor (the function that creates the object). Using the String
‘s prototype
property, we can add new methods to String
objects. Consider this example:
function consonantize(){
var consonants ="";
for(i=0;i<this.length;i++){
var l = this.charAt(i);
if(l!="a" && l!="A" && l!="e" && l!="E" &&
l!="i" && l!="I" && l!="o" && l!="O" && l!="u" && l!="U" && l!="
"){
consonants+=l;
}
}
return consonants;
}
The above function goes through a string and removes all vowels and spaces, returning only consonants. Now, we can use it on any String
object, or any String
primitive datum:
String.prototype.consonantize = consonantize;
var dg = "Danny Goodman";
var df = new String("David Flanagan");
alert(dg.consonantize());
alert(df.consonantize());
Neat, huh? Note how the new method, just like other String
methods, can be used by a String
object or by a String
primitive data type. Therefore, by using an object constructor’s prototype
method, we can add properties and methods to both native objects and user-defined objects.
Constructor
Every instance of an object has a constructor property. It returns the Function
object that created that instance of the object. For example:
function myConstructor(){
}
var str = new String("Some String");
var obj = new Object();
var myObj = new myConstructor();
alert(str.constructor); // the native String() constructor
alert(String) // the native String() constructor
alert(obj.constructor); // the native Object() constructor
alert(Object) // the native Object() constructor
alert(myObj.constructor); // the user-defined myConstructor() constructor
alert(myConstructor); // the user-defined myConstructor() constructor
I recommend that you run this example to see what it returns. Notice how each alert returns the Function
object that created that instance of the object. Also, notice that JavaScript’s native objects return "[native code]". When you retrieve the typeof
for a constructor property, you’ll find that it’s the same as the Function
object that created it, "function
":
alert(typeof str.constructor); // "function"
alert(typeof String) // "function"
alert(typeof obj.constructor); // "function"
alert(typeof Object) // "function"
alert(typeof myObj.constructor); // "function"
alert(typeof myConstructor); // "function"
All of the above return "function
". Because a constructor property returns a reference to the Function
object that created it, the constructor is in fact a constructor method:
function myConstructor(){
var x = "y";
this.x = "x";
return x;
}
var myObj = new myConstructor();
alert(myObj.constructor); // the myConstructor() function object
alert(myObj.constructor()); // "y"
Note that in this example, we return the local variable, x
, rather than the object’s property, this.x
. So, if every object has a constructor method, and every method is really a Function
object, what’s a Function
object’s constructor?
alert(myConstructor.constructor);
alert(myObj.constructor.constructor);
alert(myConstructor.constructor.constructor);
alert(myObj.constructor.constructor.constructor);
All of those return the native Function()
object constructor. Although that’s trivial, I personally thought it rather interesting- and thought you might too, and it brings me to another point. Constructors are both "types of objects" as well as objects themselves (more specifically, Function
objects). Thus, Date is both an object (a Function
object) and a "type of object", from which you can create Date
objects, or instances of the Date
object. This is true for all native objects and user-defined objects.
The practical value of all this is that, via an object’s constructor method, we can figure out what type of object it is. We can see whether it’s a String
object, created from the native String
constructor function; whether it’s an Object
object, created from the native Object
constructor function; or whether it’s one of our user-defined objects, created from a user-defined constructor function.
Besides being a method of an object, constructor()
is also a method of a primitive data type. So what does it return? After all, no real constructor function was run to create primitive data types:
var primitiveString1 = "This is a primitive string";
var primitiveString2 = String("This is a primitive string");
var stringObject = new String("This is a String object");
primitiveString1.prop = "This is a property";
primitiveString2.prop = "This is a property";
stringObject.prop = "This is a property";
alert(primitiveString1.prop) // "undefined"
alert(primitiveString2.prop) // "undefined"
alert(stringObject.prop) // "This is a property"
alert(typeof primitiveString1); // "string"
alert(typeof primitiveString2); // "string"
alert(typeof stringObject) // "object"
alert(primitiveString1.constructor); // "function String(){
[native code] }"
alert(primitiveString2.constructor); // "function String(){
[native code] }"
alert(stringObject.constructor); // "function String(){
[native code] }"
As we can see, both a String
primitive data type and a String
object have the same constructor()
, the native String()
constructor. Note that constructor()
is the only property/method that a primitive data type holds, so these data types have access to the properties/methods defined in the native object constructor function. For example, a primitive String
data type (as well as a String
object) has access to the many properties/methods defined in the native String()
constructor, including:
- length
- anchor()
- big()
- bold()
- charAt()
- charCodeAt()
- concat()
- indexOf()
- lastIndexOf()
- sub()
- substr()
- substring()
However, a String
object may also contain properties/methods that are particular to that object. For example:
var myStringObj = new String("This is a String object");
myStringObj.prop = "This is a property of the object I created";
alert(myStringObj.prop) // "This is a property of the object I created"
As Alex Vincent notes, sometimes you’ll want to turn a primitive data type into an object. For example, let’s say we have a function like this:
function myFunc(param){
param.property = "I want to add this property";
alert(param.property); // "undefined"
}
If we decide to use this function and pass it a primitive data type, we can’t also add properties to it, because it’s not an object. And anyway, passing an object is rather cumbersome:
myFunc(new String("This is a String object"));
myFunc(new Number(5));
One way to overcome this, as Alex points out, is as follows:
function myFunc(param){
param = new param.constructor(param);
param.property = "I want to add this property";
alert(param.property); // returns "I want to add this property"
}
That new line looks confusing, but let’s take a step back. Imagine that we wanted to change a primitive Number into a new Number
object. We could use:
var myNum = 5;
myNum = new Number(5);
Now let’s take that a step further:
var myNum = 5;
myNum = new myNum.constructor(5);
You must remember that myNum.constructor()
is the same as Number()
. Then, instead of using 5, we can use myNum
, as that, too, is 5:
var myNum = 5;
myNum = new myNum.constructor(myNum);
And the same works for a String primitive data type – as it does for all primitive data types. Therefore, when we pass any primitive data type as an argument to our function, we automatically convert it to an object so that we can add properties/methods to it.
Prototype Revisited
Let’s go back and revisit the Function
object’s prototype
property. In Java, a popular, well-known feature is to extend a class; however, in JavaScript, most people are unaware that you can do this – but you can! For instance, let’s say we have a Car
object. A Corvette
and an Ares
are two different types of cars, but they are both still cars. In this way, they have similar properties/methods and extend upon the Car
object.
Let’s create the three objects we’re going to use – Car, Corvette
, and Ares
. Then, we’ll discuss the ways for the latter two to inherit the properties/methods of the Car
object.
function Car(color){
this.wheels = 4;
this.doors = 4;
this.color = color;
this.speed = 0;
this.accelerate = function(){
this.speed+=20;
}
this.brake = function(){
this.speed-=20;
}
}
function Corvette(color){
// all of Car properties/methods
this.doors = 2;
this.color = color;
this.accelerate = function(){
this.speed+=40;
}
}
function Ares(color){
// all of Car properties/methods
this.doors = 2;
this.color = color;
this.accelerate = function(){
this.speed+=10;
}
this.brake = function(){
this.speed-=10;
}
}
var myCar = new Car("white");
var myCorvette = new Corvette("black");
var myAres = new Ares("red");
Because a Corvette is an especially fast car, we’ve upped its acceleration speed from a normal car, and because a Dodge Ares is a rickety, old car, we’ve made it so the brakes don’t work as well, and it doesn’t accelerate as fast (no offense to Dodge Ares owners). Now, we could use the Corvette()
and Ares()
prototype property and add to each the properties/methods from the Car
object that we want them to inherit. However, this could be a confusing and tedious task, especially if there are many properties/methods. To overcome this, we need to examine the prototype
property again.
The prototype
property is an object with no initial properties/methods. When we add properties/methods to this object, we automatically add them to all instances of the object. However, instead of adding properties/methods to the prototype
object, we could replace the prototype object with an object that already has the properties/methods we want. For example, instead of using:
Corvette.prototype.wheels = 4;
Corvette.prototype.speed = 0;
Corvette.prototype.brake = function(){
this.speed-=20;
}
we can more easily use:
Corvette.prototype = new Car();
We can do the same for the Ares
object:
Ares.prototype = new Car();
Both the Corvette
and Ares
objects now have all the Car
‘s properties/methods, which can then be overridden by the properties/methods defined in each object constructor. For example, in both the Corvette
and Ares
objects, the door property is overridden to 2
. Altogether, our now code looks like:
function Car(color){
this.wheels = 4;
this.doors = 4;
this.color = color;
this.speed = 0;
this.accelerate = function(){
this.speed+=20;
}
this.brake = function(){
this.speed-=20;
}
}
function Corvette(color){
this.doors = 2;
this.color = color;
this.accelerate = function(){
this.speed+=40;
}
}
Corvette.prototype = new Car();
function Ares(color){
this.doors = 2;
this.color = color;
this.accelerate = function(){
this.speed+=10;
}
this.brake = function(){
this.speed-=10;
}
}
Ares.prototype = new Car();
var myCar = new Car("white");
var myCorvette = new Corvette("black");
var myAres = new Ares("red");
Now, from the Corvette
and Ares
objects, we can retrieve the appropriate properties and run the accelerate()
and brake()
methods that correspond to those objects. In this way, in JavaScript, object inheritance is not hard to accomplish.
Wrap-up
Through this tutorial, I hope you’ve learned a general understanding of how JavaScript operates. In addition, I hope you’ve gained a basic knowledge of OOP and an understanding of the power of JavaScript as an object-based language. I suggest that you post any questions you might have in the SitePoint Forums; however, if you can’t seem to find an answer to your JavaScript object question, I’d be more than happy to give it a shot if you email me at arielladog@yahoo.com
There have been many people who have helped me write this tutorial. In particular, though, I’d like to thank Alex Vincent, Jason Davis, and Jared for helping me to understand the finer points of JavaScript’s object abilities.