Introduction
The concept of a class is fundamental in software application development, for being able to represent the (business) object types of an application domain such as customers, items and orders in the domain of purchasing, or students, courses and lecture rooms in the domain of university management.
Therefore, object-oriented programming languages, such as Java and C#, providing concepts of classes and class hierarchies, have become the predominant technology in software application engineering.
Objects instantiate (or are classified by) a class. A class defines the properties and methods (as a blueprint) for the objects created with it. Having a class concept is essential for being able to implement a data model in the form of model classes in modern software applications, especially when using a Model-View-Controller (MVC) architecture. Many popular JS frameworks, such as AngularJS or ReactJS, are strong in their view support (focus on the user interface code), but are weak in their model supprt (do not provide any concept of model classes). cLASSjs (like mODELcLASSjs) may be used as their model component.
However, there is no explicit class concept in JavaScript. Therefore, classes have to be defined using certain code patterns. The classical code pattern for defining classes in JavaScript, as recommended by Mozilla in their JavaScript Guide, requires four steps, as shown below, for defining a simple class hierarchy, like defining the class Student
as a subclass of Person
.
In this article, I show how using cLASSjs simplifies the definition of classes and helps to avoid the boilerplate code otherwise needed. Classes are defined in the form of ordinary constructor functions as instances of (the meta-class) cLASS. The main benefits of using cLASSjs are:
- You can use an intuitive syntax for defining a class or class hierarchy, with properties and methods, in a declarative way.
- You do not need to look up the hard-to-remember multi-step Mozilla code pattern for defining a class or class hierarchy.
- Your class definition code becomes more concise and readable, without boilerplate code.
- You may define meta-data, such as a label or an initial value, for properties.
Here is an example, how to define the class Student
as a subclass of Person
with the help of cLASSjs:
var Person = new cLASS({
Name:"Person",
properties: {
"first": {label:"First name"},
"last": {label:"Last name"}
},
methods: {
"getInitials": function () {
return this.first.charAt(0) + this.last.charAt(0);
}
}
});
var Student = new cLASS({
Name:"Student",
supertypeName:"Person",
properties: {
"studNo": {label:"Student number", initialValue: 101}
}
});
Notice that the meta-property cLASS::Name
is capitalized, violating the property naming convention used otherwise. This exception from the convention is justified because we cannot use the standard (lower case) form "name", which is already pre-defined by JS as Function::name, which is a frozen property, that is, it cannot be reset.
Compare the concise cLASS-based code above with the following hard-to-remember standard code pattern for defining constructor-based classes.
Step 1.a) 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( fN, lN) {
this.first = fN;
this.last = lN;
}
Notice that within a constructor, the special variable this
refers to the new object that is created when the constructor is invoked.
Step 1.b) Next, define the (instance-level) methods of the class as method slots of the object referenced by the constructor's prototype
property:
Person.prototype.getInitials = function () {
return this.first.charAt(0) + this.last.charAt(0);
}
Step 2.a): Define a subclass with additional properties:
function Student( fN, lN, studNo) {
Person.call( this, fN, lN);
this.studNo = studNo;
}
By invoking the supertype constructor with Person.call( this, ...)
for any new object created, and referenced by this
, as an instance of the subtype Student
, we achieve that the property slots created in the supertype constructor (first
and last
) 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 Step 2b), 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;
With Object.create( Person.prototype)
we create a new object with Person.prototype
as its prototype and without any own property slots. By assigning this object to the prototype
property of the subclass constructor, we achieve that the methods defined in, and inherited from, the superclass are also available for objects instantiating the subclass. This mechanism of chaining the prototypes takes care of method inheritance. Notice that setting Student.prototype
to Object.create( Person.prototype)
is preferable over setting it to new Person()
, which was the way to achieve the same in the time before ES5.
Background
As explained in my JavaScript Summary, 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 declared 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 non-direct 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.
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 a class with the help of the new
operator. This is the classical approach recommended by Mozilla in their JavaScript Guide (and implemented in the ES6 class
syntax).
-
In the form of a factory object that uses the predefined Object.create
method for creating new instances of a class. In this approach, the constructor-based inheritance mechanism is replaced by another mechanism. Eric Elliott has argued that factory-based classes are a viable alternative to constructor-based classes in JavaScript. He even condemns the use of classical inheritance with constructor-based classes, throwing out the baby with the bath water.
When building an app, we can use both types 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 the advantages of supporting multiple inheritance and the use of object pools, the constructor-based approach enjoys the advantage of higher performance object creation.
Using cLASSjs
In this section, I show how to use cLASS for defining constructor-based classes with prototype-based method inheritance.
As shown in the attached example file test_cLASS.html
, the cLASSjs library has to be added to the JS code of your page, for instance by adding a JS code loading element like
<script src="../cLASS.js"></script>
You can then define classes, possibly forming a class hierarchy, as shown above with the example of the class Student
being defined as a subclass of Person
.
Defining a new object
Since a cLASS like Student
is an ordinary constructor function (enriched with a few meta-data properties), you can define new objects in the familiar way with new
:
var s1 = new Student({first:"Tom", last:"Sawyer"});
Using instance-level methods
cLASSjs provides a pre-defined toString()
method that improves JavaScript's default Object.prototype.toString()
method by using class names and property labels for serializing typed objects like s1
. The following piece of code invokes this method on s1
and displays the result in the first p
element of the page:
var pElems = document.getElementsByTagName("p");
pElems[0].textContent = s1.toString();
pElems[1].textContent = "Initials: "+ s1.getInitials();
The second p
element shows the result of invoking the getInitials()
method that the object s1
has inherited from the Person
class.
Generating form fields for CRUD user interfaces
cLASSjs provides a few meta-data properties for cLASSes, which can be used, for instance, for generating CRUD user interfaces where form fields are bound to properties and form action elements (such as buttons) are bound to methods.
This approach is illustrated in the following piece of code where we assume that the page contains a form element with id value "f1". We loop over all properties of the class Student
, which can be retrieved with the help of the meta-property Student.properties
:
var formElem = document.forms["f1"];
Object.keys( Student.properties).forEach( function (p) {
var labelElem = null, inputElem = null;
labelElem = document.createElement("label");
labelElem.textContent = Student.properties[p].label;
inputElem = document.createElement("input");
inputElem.name = p;
inputElem.value = s1[p];
labelElem.appendChild( inputElem);
formElem.appendChild( labelElem);
});
Possible extensions
cLASSjs can be easily extended by adding further meta-data properties, e.g., for specifying property constraints that can be checked with a generic constraint checking method on user input or before form submission in CRUD user interfaces using the HTML5 constraint validation API, in the same way as in mODELcLASSjs.
Acknowledgements
I'm grateful to Florent Steiner for pointing out a bug in the first version and making a useful suggestion how to improve the code for including the supercLASS properties in the properties map of a subcLASS.
History
- 2016-01-19: add an example of using the classical code pattern for defining constructor-based classes.
- 2016-01-04: a) rename the meta-property cLASS::
typeName
to cLASS::Name
for making it more intuitve; b) improve cLASSjs code for inheriting the superclass properties as suggested by Florent Steiner (see comments below). - 2015-12-30: corrected a bug pointed out by a reader.
- 2015-12-29: first version created.