The concept of a class is fundamental in object-oriented programming. Objects instantiate (or are classified by) a class. A class defines the properties and methods (as a blueprint) for the objects that instantiate it. Having a class concept is essential for being able to implement a data model in the form of model classes. However, classes and their inheritance/extension mechanism are over-used in classical OO languages, such as in Java, where all variables and procedures have to be defined in the context of a class. Consequently, classes are not only used for implementing object types (or model classes), but also as containers for many other purposes in these languages. This is not the case in JavaScript where we have the freedom to use classes for implementing object types only, while keeping method libraries in namespace objects.
Any code pattern for defining classes in JavaScript should satisfy five requirements. First of all, (1) it should allow to define a class name, a set of (instance-level) properties, preferably with the option to keep them 'private', a set of (instance-level) methods, and a set of class-level properties and methods. It's desirable that properties can be defined with a range/type, and with other meta-data, such as constraints. There should also be two introspection features: (2) an is-instance-of predicate that can be used for checking if an object is a direct or indirect instance of a class, and (3) an instance-level property for retrieving the direct type of an object. In addition, it is desirable to have a third introspection feature for retrieving the direct supertype of a class. And finally, there should be two inheritance mechanisms: (4) property inheritance and (5) method inheritance. In addition, it is desirable to have support for multiple inheritance and multiple classifications, for allowing objects to play several roles at the same time by instantiating several role classes.
There is no explicit class concept in JavaScript. Different code patterns for defining classes in JavaScript have been proposed and are being used in different frameworks. But they do often not satisfy the five requirements listed above. The two most important approaches for defining classes are:
-
In the form of a constructor function that achieves method inheritance via the prototype chain and allows to create new instances of the class with the help of the new
operator. This is the classical approach recommended by Mozilla in their JavaScript Guide.
-
In the form of a factory object that uses the predefined Object.create
method for creating new instances of the class. In this approach, the prototype chain method inheritance mechanism is replaced by a copy&append mechanism. Eric Elliott has argued that factory-based classes are a viable alternative to constructor-based classes in JavaScript (in fact, he even condemns the use of classical inheritance and constructor-based classes, throwing out the baby with the bath water).
When building an app, we can use both kinds of classes, depending on the requirements of the app. Since we often need to define class hierarchies, and not just single classes, we have to make sure, however, that we don't mix these two alternative approaches within the same class hierarchy.While the factory-based approach, as exemplified by mODELcLASSjs, has many advantages, which are summarized in Table 1, the constructor-based approach enjoys the advantage of higher performance object creation.
Table 1. Required and desirable features of JS code patterns for classes
Class feature | Constructor-based approach | Factory-based approach | mODELcLASSjs |
Define properties and methods | yes | yes | yes |
Declare properties with a range (and other meta-data) | no | possibly | yes |
Built-in is-instance-of predicate | yes | yes | yes |
Built-in direct type property | yes | yes | yes |
Built-in direct supertype property of classes | no | possibly | yes |
Property inheritance | yes | yes | yes |
Method inheritance | yes | yes | yes |
Multiple inheritance | no | possibly | yes |
Multiple classifications | no | possibly | yes |
Allow object pools | no | yes | yes |
Constructor-based classes
A constructor-based class can be defined in two or three steps. First define the constructor function that implicitly defines the properties of the class by assigning them the values of the constructor parameters when a new object is created:
function Person( first, last) {
this.firstName = first;
this.lastName = last;
}
Next, define the instance-level methods of the class as method slots of the constructor's prototype
property:
Person.prototype.getInitials = function () {
return this.firstName.charAt(0) + this.lastName.charAt(0);
}
Finally, class-level ("static") methods can be defined as method slots of the constructor function itself, as in
Person.checkName = function (n) {
...
}
An instance of such a constructor-based class is created by applying the new
operator to the constructor function and providing suitable arguments for the constructor parameters:
var pers1 = new Person("Tom","Smith");
The method getInitials
is invoked on the object pers1
of type Person
by using the 'dot notation':
alert("The initials of the person are: " + pers1.getInitials());
When a typed object o
is created with o = new C(
...)
, where C
references a named function with name "C", the type (or class) name of o
can be retrieved with the introspective expression o.constructor.name
. which returns "C" (however the Function::name
property used in this expression is not supported by Internet Explorer up to the current version 11).
For defining a subclass in a constructor-based class hierarchy, we use a 3-part code pattern, as recommended by Mozilla in their JavaScript Guide. A class Student
is defined as a subclass of Person
in the following way. The first step is the definition of the superclass Person
above. The second step is the definition of the subclass Student
like so:
function Student( first, last, studNo) {
Person.call( this, first, last);
this.studNo = studNo;
}
By invoking the supertype constructor with Person.call( this, ...)
for any new object created (referenced by this
) as an instance of the subtype Student
, we achieve that the property slots created in the supertype constructor (firstName
and lastName
) are also created for the subtype instance, along the entire chain of supertypes within a given class hierarchy. In this way we set up a property inheritance mechanism that makes sure that the own properties defined for an object on creation include the own properties defined by the supertype constructors.
In the third step, we set up a mechanism for method inheritance via the constructor's prototype
property. We assign a new object created from the supertype's prototype
object to the prototype
property of the subtype constructor and adjust the prototype's constructor property:
Student.prototype = Object.create( Person.prototype);
Student.prototype.constructor = Student;
By assigning an empty supertype instance to the prototype property of the subtype constructor, we achieve that the methods defined in, and inherited by, the supertype are also available for objects instantiating the subtype. This mechanism of chaining the prototypes takes care of method inheritance. Notice that setting Student.prototype
to Object.create( Person.prototype)
, which creates a new object with its prototype
set to Person.prototype
and without any own properties, is preferable over setting it to new Person()
, which was the way to achieve the same in the time before ECMAScript 5.
Finally, we define the additional methods of the subclass as method slots of its prototype
object:
Student.prototype.setStudNo = function (studNo) {
this.studNo = studNo;
}
As shown below in Figure 1, every constructor function has a reference to a prototype object as the value of its prototype
property. When an object is created with the help of new
, its (unofficial) built-in reference property __proto__
(with a double underscore prefix and suffix) is set to the value of the constructor's prototype
property. For instance, after creating a new object with f = new Foo()
, it holds that Object.getPrototypeOf( f)
, which is the same as f.__proto__
, is equal to Foo.prototype
. Consequently, changes to the slots of Foo.prototype
affect all objects that were created with new Foo()
. While every object has a __proto__
reference property (except Object
), only objects constructed with new
have a constructor
reference property.
Figure 1. The built-in JavaScript classes Object
and Function
.
This post has been extracted from the book Building Front-End Web Apps with Plain JavaScript.