JavaScript Class Patterns
- by Liam McLennan
To write object-oriented programs we need objects, and likely lots of them. JavaScript makes it easy to create objects: var liam = {
name: "Liam",
age: Number.MAX_VALUE
};
But JavaScript does not provide an easy way to create similar objects. Most object-oriented languages include the idea of a class, which is a template for creating objects of the same type. From one class many similar objects can be instantiated.
Many patterns have been proposed to address the absence of a class concept in JavaScript. This post will compare and contrast the most significant of them.
Simple Constructor Functions
Classes may be missing but JavaScript does support special constructor functions. By prefixing a call to a constructor function with the ‘new’ keyword we can tell the JavaScript runtime that we want the function to behave like a constructor and instantiate a new object containing the members defined by that function. Within a constructor function the ‘this’ keyword references the new object being created - so a basic constructor function might be:
function Person(name, age) {
this.name = name;
this.age = age;
this.toString = function() {
return this.name + " is " + age + " years old.";
};
}
var john = new Person("John Galt", 50);
console.log(john.toString());
Note that by convention the name of a constructor function is always written in Pascal Case (the first letter of each word is capital). This is to distinguish between constructor functions and other functions. It is important that constructor functions be called with the ‘new’ keyword and that not constructor functions are not.
There are two problems with the pattern constructor function pattern shown above:
It makes inheritance difficult
The toString() function is redefined for each new object created by the Person constructor. This is sub-optimal because the function should be shared between all of the instances of the Person type.
Constructor Functions with a Prototype
JavaScript functions have a special property called prototype. When an object is created by calling a JavaScript constructor all of the properties of the constructor’s prototype become available to the new object. In this way many Person objects can be created that can access the same prototype. An improved version of the above example can be written:
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype = {
toString: function() {
return this.name + " is " + this.age + " years old.";
}
};
var john = new Person("John Galt", 50);
console.log(john.toString());
In this version a single instance of the toString() function will now be shared between all Person objects.
Private Members
The short version is: there aren’t any. If a variable is defined, with the var keyword, within the constructor function then its scope is that function. Other functions defined within the constructor function will be able to access the private variable, but anything defined outside the constructor (such as functions on the prototype property) won’t have access to the private variable. Any variables defined on the constructor are automatically public. Some people solve this problem by prefixing properties with an underscore and then not calling those properties by convention.
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype = {
_getName: function() {
return this.name;
},
toString: function() {
return this._getName() + " is " + this.age + " years old.";
}
};
var john = new Person("John Galt", 50);
console.log(john.toString());
Note that the _getName() function is only private by convention – it is in fact a public function.
Functional Object Construction
Because of the weirdness involved in using constructor functions some JavaScript developers prefer to eschew them completely. They theorize that it is better to work with JavaScript’s functional nature than to try and force it to behave like a traditional class-oriented language. When using the functional approach objects are created by returning them from a factory function. An excellent side effect of this pattern is that variables defined with the factory function are accessible to the new object (due to closure) but are inaccessible from anywhere else. The Person example implemented using the functional object construction pattern is:
var john = new Person("John Galt", 50);
console.log(john.toString());
var personFactory = function(name, age) {
var privateVar = 7;
return {
toString: function() {
return name + " is " + age * privateVar / privateVar + " years old.";
}
};
};
var john2 = personFactory("John Lennon", 40);
console.log(john2.toString());
Note that the ‘new’ keyword is not used for this pattern, and that the toString() function has access to the name, age and privateVar variables because of closure.
This pattern can be extended to provide inheritance and, unlike the constructor function pattern, it supports private variables. However, when working with JavaScript code bases you will find that the constructor function is more common – probably because it is a better approximation of mainstream class oriented languages like C# and Java.
Inheritance
Both of the above patterns can support inheritance but for now, favour composition over inheritance.
Summary
When JavaScript code exceeds simple browser automation object orientation can provide a powerful paradigm for controlling complexity. Both of the patterns presented in this article work – the choice is a matter of style. Only one question still remains; who is John Galt?