Submitted by gwagner on
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 within a Model-View-Container (MVC) architecture. 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 and, 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 |
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) { // invoke superclass constructor Person.call( this, first, last); // define and assign additional properties 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:
// inherit from Person Student.prototype = Object.create( Person.prototype); // adjust the subtype's constructor property 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.