Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / HTML

Simplifying the Definition of Constructor-Based Classes with cLASSjs

4.43/5 (4 votes)
18 Jan 2016CPOL8 min read 22.9K   52  
cLASSjs is a small (2 kB) JS library for defining constructor-based classes with prototype-based method inheritance like so: Student = new cLASS({ Name:Student, supertypeName:Person, properties:..., methods:...}).

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:

JavaScript
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:

JavaScript
function Person( fN, lN) {
  this.first = fN;  // First name
  this.last = lN;   // Last name
}

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:

JavaScript
Person.prototype.getInitials = function () {
  return this.first.charAt(0) + this.last.charAt(0);
}

Step 2.a): Define a subclass with additional properties:

JavaScript
function Student( fN, lN, studNo) {
  // invoke superclass constructor
  Person.call( this, fN, lN);
  // define and assign additional properties
  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:

JavaScript
// Student inherits from Person
Student.prototype = Object.create( 
    Person.prototype
);
// adjust the subtype's constructor property
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:

  1. 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).

  2. 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

JavaScript
<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:

JavaScript
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:

JavaScript
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:

JavaScript
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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)