While JavaScript is by design an object-oriented language, it hasn't
always been the easiest language to create nicely structured code that
is easily maintainable.
Writing JavaScript Classes with Prototype
Introduction
While JavaScript is by design an object-oriented language, it hasn't
always been the easiest language to create nicely structured code that
is easily maintainable. Prototype helps with this by providing a number
of useful features for creating classes, such as the ability to extend
classes and to easily group all functions together by using function
binding. In this article I will show you how to create classes using
JavaScript and Prototype.
To begin with, I'll show you how to define classes and how to create
the class constructor. Next I will show you how to create child classes
by extending your classes. After this I will introduce you to function
binding and how it applies to the development of JavaScript classes.
Next I will show to create a string representation of instances of
your created classes, then how to create custom enumerables (as
mentioned in the third article in this series).
This article assumes you have read and understood the first five
articles in this series, and also that you have at least a basic
knowledge of how object-oriented programming (OOP) works.
Declaring Classes
To create a new class with Prototype, the Class.create()
function is used. This method accepts an object as the first argument,
with each element of that object corresponding to a class method. The
created class is returned from the call to Class.create().
Listing 1 shows an example of creating a new class called Person.
An instance of this class represents a single person. I will build on
this particular class as we continue through this article.
This class declares one property (name), in which the person's name is held. It also declares two functions; one for setting the name (setName()) and another for retrieving the name (getName()).
Listing 1 Creating a basic JavaScript class (listing-1.html)
<html>
<head>
<title>Creating a Basic JavaScript Class</title>
<script type="text/javascript" src="/js/prototype.js"></script>
</head>
<body>
<script type="text/javascript">
var Person = Class.create({
name : null,
setName : function(name)
{
this.name = name;
},
getName : function()
{
return this.name;
}
});
var me = new Person();
me.setName('Quentin Zervaas');
alert(me.getName());
</script>
</body>
</html>
Declaring the name property is not required (you can
just assign directly to the variable to create it), but doing so makes
it clearer that the property exists.
One important facet of creating classes in this manner is you must
remember that each function is an object element (the first argument to
Class.create()), which therefore means you must separate each element by comma. Think of it as Class.create({ function1, function2, function3 }).
As you can see in Listing 1, once the class has been declared we instantiate it using the new keyword, just as you would with most other OOP languages.
Saving and Accessing Classes
My own personal preference when saving classes is to save each class to a separate file, using the filename ClassName.js.
Note: Additionally, when I want to namespace (that is, group classes into separate packages) I will name the class PackageName_ClassName and save the file to PackageName/ClassName.js.
Therefore, re-working the Person class into a
separate file would result in the code in Listing 2. We can then load
the class file and instantiate the class as shown in Listing 3.
Listing 2 Declaring the Person class in an external file (Person.js)
var Person = Class.create({
name : null,
setName : function(name)
{
this.name = name;
},
getName : function()
{
return this.name;
}
});
Listing 3 Loading and instantiating the Person class (listing-3.html)
<html>
<head>
<title>Loading and instantiating the Person class</title>
<script type="text/javascript" src="/js/prototype.js"></script>
<script type="text/javascript" src="/js/Person.js"></script>
</head>
<body>
<script type="text/javascript">
var me = new Person();
me.setName('Quentin Zervaas');
alert(me.getName());
</script>
</body>
</html>
You should always be declaring your classes in external files and
not inline in your HTML. In fact, you should almost never include any
JavaScript code in your HTML files.
Defining the Class Constructor
In Object-Oriented Programming, the class constructor is a method
that is executed when the class is instantiated. By default JavaScript
doesn't use constructors when you create classes, but Prototype allows
us to do so by creating a function called initialize().
When you create a new instance of your class the initialize() function will be executed. You can pass arguments to the constructor by including them when you instantiate the class.
To demonstrate this, I'll now modify the Person class so rather than having to set the name by calling setName() after you create a Person object, you can set the name in the constructor.
Listing 4 shows the new version of the Person.js file, while Listing 5 shows how you can now instantiate the class and set the name without explicitly calling setName().
Listing 4 The Person class with a constructor which sets the name (Person.js)
var Person = Class.create({
name : null,
initialize : function(name)
{
this.setName(name);
},
setName : function(name)
{
this.name = name;
},
getName : function()
{
return this.name;
}
});
Listing 5 Defining the person's name when instantiating the Person class (listing-5.html)
<html>
<head>
<title>Loading and instantiating the Person class</title>
<script type="text/javascript" src="/js/prototype.js"></script>
<script type="text/javascript" src="/js/Person.js"></script>
</head>
<body>
<script type="text/javascript">
var me = new Person('Quentin Zervaas');
alert(me.getName());
</script>
</body>
</html>
Inheritance
A child class is one that extends from another class, meaning the
properties and methods of the parent class now also belong to the
child. Additionally, the child class can overwrite any methods as it
sees fit, or it can create its own methods that don't exist in the
parent.
While JavaScript doesn't provide OOP features such as abstract
classes or interfaces, Prototype does make it fairly straightforward to
create child classes.
Note: Technically it's possible to
emulate abstract classes by creating a method in the abstract class
that throws an exception. This exception will not occur if the child
class has its own implementation of the method.
To create a child class, you still use the Class.create()
method, but rather than passing the class methods as the first
argument, you pass them as the second argument, and instead you pass
the name of the parent class as the first argument. This is
demonstrated in Listing 6.
According to the naming rules mentioned earlier in the article, my preference is to call this class Person_Australian (since it becomes part of the Person package) and to save it in a directory called Person (./Person/Australian.js).
Listing 6 Creating a sub-class of Person (Australian.js)
var Person_Australian = Class.create(Person, {
});
Because Prototype isn't really designed to have class inheritance,
you must make the small concession of adding an extra parameter to each
method you want to overwrite in child classes. This extra parameter is
called $super and it is the first argument to the method.
It is in fact a variable holding the parent method of the same name,
meaning you can call $super().
Listings 7 and 8 demonstrate this by adding a method called getCountry() to the Person class. In the Person_Australian class we override this method so we can return the name of the country. As you can see, the method accepts $super
as its first (and in this case, only) argument; however when you call
this method you still don't use any arguments since Prototype
internally passes the $super argument. This is demonstrated in Listing 9 which loads and instantiates the two classes.
Note: Since the declaration of the Person_Australian class relies on the Person class, you must be sure to load the Person.js file before Australian.js.
Listing 7 Declaring the parent getCountry() method (Person.js)
var Person = Class.create({
name : null,
initialize : function(name)
{
this.setName(name);
},
setName : function(name)
{
this.name = name;
},
getName : function()
{
return this.name;
},
getCountry : function()
{
return 'Unknown';
}
});
Listing 8 Overriding getCountry() in the child class (Australian.js)
var Person_Australian = Class.create(Person, {
getCountry : function($super)
{
return 'Australia, not ' + $super();
}
});
Listing 9 Loading and instantiating the Person and Person_Australian classes (listing-9.html)
<html>
<head>
<title>Loading and instantiating the Person and Person_Australian classes</title>
<script type="text/javascript" src="/js/prototype.js"></script>
<script type="text/javascript" src="/js/Person.js"></script>
<script type="text/javascript" src="/js/Person/Australian.js"></script>
</head>
<body>
<script type="text/javascript">
var you = new Person('Some Person');
alert(you.getCountry());
// displays "Unknown"
var me = new Person_Australian('Quentin Zervaas');
alert(me.getCountry());
// displays "Australia, not Unknown"
</script>
</body>
</html>
Function Binding
When using Prototype to develop classes, you need to have an
understanding of what function binding is and how to use it.
Essentially what binding does is instruct what the variable this refers to in a function. This is especially useful for event handling and for handling Ajax responses.
As I showed you in the fourth article of this series ("Event Handling in Prototype"), to observe an event on an element you use theElement.observe('eventName', handler).
If you are developing a class then you want your handler function to be
one of your class methods. This is primarily so you can access other
class methods when handling the event.
Consider the code in Listing 10. In this example, when the user clicks the button the _onButtonClick function is called. Our aim is to display the string returned by the getMessage() method when the button is clicked.
Note: My personal preference is to
name event handlers using an underscore since the method shouldn't
directly be called. Typically this format is used to indicate protected
or private methods.
This code will not yet work, as explained following the listing!
Listing 10 Demonstrating the drawback of not using function binding (listing-10.html)
<html>
<head>
<title>Demonstrating the drawback of not using function binding</title>
<script type="text/javascript" src="/js/prototype.js"></script>
</head>
<body>
<div>
<input type="button" value="Click Me!" id="myButton" />
</div>
<script type="text/javascript">
var MyClass = Class.create({
initialize : function(button)
{
button = $(button);
button.observe('click', this._onButtonClick);
},
getMessage : function()
{
return 'This is a simple function';
},
_onButtonClick : function(e)
{
var button = Event.element(e);
var message = this.getMessage();
button.up().update(message);
}
});
new MyClass('myButton');
</script>
</body>
</html>
The problem that occurs in this example is that when _onButtonClick runs, the getMessage() method is not found. This is because the keyword this doesn't refer to the instance of MyClass.
To solve this problem, Prototype provides two methods: bind() and bindAsEventListener(). They are basically the same thing, the difference being that you should use bindAsEventListener() specifically when you are observing events, because then Prototype knows to pass in the event object to the callback handler.
Listing 11 demonstrates usage of the bindAsEventListener(). This method accepts a single argument: the variable to bind the function to.
Listing 11 Binding a function with bindAsEventListener (listing-11.html)
<html>
<head>
<title>Binding variable with bindAsEventListener</title>
<script type="text/javascript" src="/js/prototype.js"></script>
</head>
<body>
<div>
<input type="button" value="Click Me!" id="myButton" />
</div>
<script type="text/javascript">
var MyClass = Class.create({
initialize : function(button)
{
button = $(button);
button.observe('click', this._onButtonClick.bindAsEventListener(this));
},
getMessage : function()
{
return 'This is a simple function';
},
_onButtonClick : function(e)
{
var button = Event.element(e);
var message = this.getMessage();
button.up().update(message);
}
});
new MyClass('myButton');
</script>
</body>
</html>
Now, inside the _onButtonClick() method, this refers to the instance of MyClass, meaning the getMessage() method can now be called.
Binding Functions for Ajax Requests
Although function binding will be most often used for event handling (using bindAsEventListener()),
you will also need to bind functions when handling the response from
Ajax requests. Since you don't need an event handler passed in (this
concept doesn't apply to Ajax requests) we use bind() instead of bindAsEventListener(), once again accepting the variable to which the function should be bound.
In the previous article in this series I demonstrated a simple example
of retrieving JSON data from the server and updating an element on the
page with the returned data. I will now rewrite this same example using
a class and function binding.
Listing 12 shows the PHP code used to return JSON data from the server.
This is the script that will be accessed in the Ajax request. For a
further explanation of how this all works, please refer to the previous
article.
Listing 12 Sending JSON data from the server (listing-12.php)
<?php
$person = array(
'name' => 'Quentin',
'country' => 'Australia'
);
header('Content-type: application/json');
echo json_encode($person);
?>
Now comes the interesting part. In Listing 13 we define a class called MyClass which accepts a single argument to the constructor. This is the button that we observe the click event on. We save the button as a class property so we can access it again the Ajax response handler. Again, we use bindAsEventListener() so we can handle the event within the class.
Listing 13 Using bind() when handling Ajax requests (listing-13.html)
<html>
<head>
<title>Using bind() when handling Ajax requests</title>
<script type="text/javascript" src="/js/prototype.js"></script>
</head>
<body>
<div>
<input type="button" value="Click Me!" id="myButton" />
</div>
<script type="text/javascript">
var MyClass = Class.create({
button : null,
initialize : function(button)
{
this.button = $(button);
this.button.observe('click', this._onButtonClick.bindAsEventListener(this));
},
_onButtonClick : function(e)
{
var options = {
method : 'get',
onSuccess : this._onSuccess.bind(this)
};
new Ajax.Request('listing-12.php', options);
},
_onSuccess : function(transport)
{
var json = transport.responseJSON;
var msg = json.name + ' is from ' + json.country;
this.button.up().update(msg);
}
});
new MyClass('myButton');
</script>
</body>
</html>
Now when we handle the click event (the _onButtonClick()
method), we once again perform an Ajax request. The difference between
doing it now and in the last article was that now we use bind() so we can use the MyClass instance in the _onSuccess() method.
When the button is clicked, an Ajax request is performed. When the request is finished, _onSuccess() handles the response and updates the button's container with the JSON data read from the Ajax request.
Binding Functions for Enumerables
In the third article in this series we covered enumerables, including the each() method. Because a separate function (known as an iterator) is called when you use each(), once again any classes you are using lose their context.
Consider the code in Listing 14. In the class constructor I have
defined an array of different colours. I then attempt to loop over them
and call the display() method for each colour. The problem is, this inside the each loop doesn't refer to the MyClass instance.
Listing 14 The problem with using each() in classes (listing-14.html)
<html>
<head>
<title>The problem with using each() in classes</title>
<script type="text/javascript" src="/js/prototype.js"></script>
</head>
<body>
<div id="container"></div>
<script type="text/javascript">
var MyClass = Class.create({
container : null,
initialize : function(container)
{
this.container = $(container);
var colors = [ 'red', 'green', 'blue' ];
colors.each(function(color) {
this.display(color);
});
},
display : function(msg)
{
this.container.insert(msg + '<br />');
}
});
new MyClass('container');
</script>
</body>
</html>
When you use the each() method, it is possible to pass a second argument in order to specify the context. That is, you would use myEnum.each(myFunction, myObject). Or in this specific case, colors.each(function(color) { ... }, this). This is shown in Listing 15.
<head>
<title>Binding enumerable function handlers</title>
<script type="text/javascript" src="/js/prototype.js"></script>
</head>
<body>
<div id="container"></div>
<script type="text/javascript">
var MyClass = Class.create({
container : null,
initialize : function(container)
{
this.container = $(container);
var colors = [ 'red', 'green', 'blue' ];
colors.each(function(color) {
this.display(color);
}, this);
},
display : function(msg)
{
this.container.insert(msg + '<br />');
}
});
new MyClass('container');
</script>
</body>
</html>
<html>
Note: Technically, you can use the bind() method if you prefer, rather than using this second argument. That is, colors.each(function() { ... }.bind(this)).
Creating a String Representation of Your Class
If you want to define a string representation of an instance of your class, you can do so by defining a class method called toString(). This method must return a string.
Effectively what this does is define what value should be returned
when your object is converted (cast) to a string. Listing 16 shows an
example of how you might choose to implement toString(). Listing 17 shows how the object is displayed when converted to a string.
Listing 16 Declaring the toString() method (Person.js)
var Person = Class.create({
name : null,
initialize : function(name)
{
this.setName(name);
},
toString : function()
{
return this.getName();
},
setName : function(name)
{
this.name = name;
},
getName : function()
{
return this.name;
},
getCountry : function()
{
return 'Unknown';
}
});
Listing 17 Converting an object to a string so it can be displayed (listing-17.html)
<html>
<head>
<title></title>
<script type="text/javascript" src="/js/prototype.js"></script>
<script type="text/javascript" src="/js/Person.js"></script>
</head>
<body>
<div id="foo"></div>
<script type="text/javascript">
var me = new Person('Quentin Zervaas');
$('foo').update(me);
</script
|