Object Oriented Programming in Javascript
An Approach to using Object Oriented Programming with JavaScript
using Netscape Navigator 4, Netscape 6, Opera 5, and Internet Explorer 4 and up
Contents
Background
For anyone coming from a background in C++ or Java or another fully featured Object Oriented language, Javascript is woefully inadequate.
Currently, Javascript does not intrinsically support:
- Data types
- Data hiding via public and private qualifiers
- Destructors / Finalizers
- Inheritance
- Interfaces
Javascript's definition is changing rapidly and we can expect it to support Classes in a more traditional way in the next year or so. But in the meantime we can, with a little forethought, create a style of Object Oriented programming in Javascript that can provide many, although not all, of the benefits of Object Oriented programming in other languages.
Example 1 Creating an Object in Javascript is as simple as:
function example1()
{
// create a 'person' object
var obj = new Object();
// add properties to the object
obj.name = '';
// add methods to the object
// note that forward declarations are not necessary
obj.getName = getName;
obj.setName = setName;
// define the object's methods
function getName()
{
return this.name;
}
function setName(name)
{
this.name = name;
}
// use the 'person' object
obj.setName('bob');
alert('Example 1: ' + obj.getName());
}
The first thing to note about Javascript's definition of an Object is that an Object's definition is determined at run time. Unlike C++ or Java, it is possible to dynamically add new properties or methods or change the binding of methods at runtime. The second thing to note is that the above means of creating an object is a pain and requires the steps to be repeated for each instance to be created.
Javascript allows us to define a 'class' of objects through the use of a constructor function that defines all instances of the 'class'.
Example 2: Let's rewrite the above example using a constructor:
// define a Person2 class
function Person2(name)
{
// initialize the member variables for this instance
this.name = name;
// initialize the member function references
this.getName = getName;
this.setName = setName;
}
// define a Person2's methods
function getName()
{
return this.name;
}
function setName(name)
{
this.name = name;
}
function example2()
{
// use an instance of the Person class
var person = new Person2('bob');
alert('Example 2: ' + person.getName());
}
The constructor allows us to easily create multiple instances of our new Person class. This approach still has some limitations however. First, every time a new instance of the class is created, each instance is assigned references to the methods for the class. Apart from the time required to assign the member references, each instance maintains distinct member function references thereby increasing the storage consumed by each instance of a class.
The prototype property of Objects in Javascript allows us to have a class-wide assignment of methods that needs to be performed only once per class and not during each creation of an instance of the class.
Example 3 - Let's rewrite our example using prototype properties:
// define a Person3 class
function Person3(name)
{
// initialize the member variables for this instance
this.name = name;
// initialize the member function references
// for the class prototype
if (typeof(_person3_prototype_called) == 'undefined')
{
_person3_prototype_called = true;
Person3.prototype.getName = getName;
Person3.prototype.setName = setName;
}
}
// define a Person3's methods
function getName()
{
return this.name;
}
function setName(name)
{
this.name = name;
}
function example3()
{
// use an instance of the Person class
var person = new Person3('bob');
alert('Example 3: ' + person.getName());
}
By using the class constructor and the prototype properties of Objects we now have a class that can be instantiated any number of times, where the method initializations are performed once and where the method references are stored only one time in the classes' prototype. A problem with this approach (especially if you are using an IDE with an interactive debugger) is the proliferation of global names. There is the global class constructor function name, the global variable determining if the class's prototype has been set, and a name for each of the class's methods. This name pollution can be quite an annoyance in addition to the need to maintain globally unique names for each of the member function definitions.
Example 4 - Javascript 1.2 and higher allow us to nest function definitions and thereby reduce the pollution of the global namespace:
// define a Person4 class
function Person4(name)
{
// initialize the member variables for this instance
this.name = name;
// initialize the member function references
// for the class prototype
if (typeof(_person4_prototype_called) == 'undefined')
{
_person4_prototype_called = true;
Person4.prototype.getName = getName;
Person4.prototype.setName = setName;
}
// define a Person4's methods
function getName()
{
return this.name;
}
function setName(name)
{
this.name = name;
}
}
function example4()
{
var person = new Person4('bob');
alert('Example 4: ' + person.getName());
}
We now have a class where all method names are local to the Class constructor. The only remaining global names are the class constructor function and the prototype called flag. Although we now have a very useful style of programming objects in Javascript, we still lack the most important feature of Object Oriented style of programming: Inheritance.
Extensible Objects
In the previous section we learned how to create Objects in Javascript in a limited fashion. The major feature that is missing is Inheritance. Inheritance can be added to Classes in Javascript using a global data structure that keeps track of parent-child relationships along with the ability of Javascript to dynamically change the prototype of a Class at runtime.
Rather than evolving the Class design step by step as in the previous section, I will present an umpteenth rewrite of our 'Person' example using the new style as implemented in xbObjects.js. xbObjects is an api for creating extensible classes in JavaScript that support inheritance and overloading of methods with the ability to explicitly call a parent class's method that has been overloaded by a child class. More documentation on xbObjects will be made available soon. Also, xbObjects is just one approach to handling classes in JavaScript.
// register the Person5 class as inheriting
// from the xbObject class. xbObject is a standard
// class that defines several standard properties and
// methods. If the parent class is not specified,
// it defaults to xbObject.
_classes.registerClass('Person5', 'xbObject');
// define a Person5 class
function Person5(name)
{
// make sure all the required prototype functions are called
// and inherit the methods of the ancestor classes
_classes.defineClass('Person5', _prototype_func);
// initialize this instances properties
this.init(name);
// the prototype function is called once
// by the defineClass method of _classes.
function _prototype_func()
{
// override the init method for this class
function init(name)
{
// explicitly chain the ancestor's initialization
// this not only initializes the ancestor's properties
// but accomplishes their inheritance as well.
// parentMethod takes as first argument the name
// of the parent method to be called, followed by any
// arguments required by the parent's method
this.parentMethod('init');
// initialize and define the properties for this class
this.name = name;
}
// Navigator 4 requires you make the assignments
// to prototypes after the function definition.
// This is not required in Netscape 6 or Internet Explorer 4+
Person5.prototype.init = init;
// define a Persons methods
function getName()
{
return this.name;
}
Person5.prototype.getName = getName;
function setName(name)
{
this.name = name;
}
Person5.prototype.setName = setName;
}
}
function example5()
{
var person = new Person5('bob');
alert('Example 5: ' + person.getName());
}
_classes is a global object that is used to maintain information about each class. It contains information about the parent class of each class as well as whether a class's prototype function has been called.
Example 6 - To extend our Person5 class to an Employee class we follow the same style of conde:
// an Employee isa Person5
_classes.registerClass('Employee', 'Person5');
function Employee(name, title)
{
_classes.defineClass('Employee', _prototype_func);
this.init(name, title);
function _prototype_func()
{
// override the init method
Employee.prototype.init = init;
function init(name, title)
{
// call Person5's init method
this.parentMethod('init', name);
// add new property for Employees
this.title = title;
}
// The Person5 Class's getName and setName methods
// are automatically inherited
// add new methods for Employees
function getTitle()
{
return this.title;
}
Employee.prototype.getTitle = getTitle;
function setTitle(title)
{
this.title = title;
}
Employee.prototype.setTitle = setTitle;
}
}
function example6()
{
var employee = new Employee('bob', 'javascript geek');
alert( employee.getName() + '\'s Job title is '
+ employee.getTitle());
}
Although the code defining classes in this fashion is somewhat different that what you are used to in C++ or Java, it does have some similarities. The use of a single global Object (_classes) with methods to dynamically declare class inheritance (registerClass) and define inherited classes (defineClass) is central to this approach. Initializers (init methods) and Finalizers (destroy methods) are chained together using the parentMethod method inherited from xbObject. parentMethod allows any method of a Parent class to be called from the Child class.
