Javascript classes have always been quite different from classes that are available in other strongly-typed Object Oriented programming languages like Java, C#, C++ etc.
There are some syntactic similarities, and it is possible to emulate many of the features of classical classes in Javascript, but it is best to understand that Javascript's classes and prototype-based inheritance mechanism are substantially different from the classes and class-based inheritance in other object oriented programming languages.
The ECMAScript 2015 Language Specification
introduced syntax element and provided constructors
, static
, instance methods, inheritance and the ability to
extend a class from other classes.
The class
syntax, is just syntactic sugar
concealing Javascript prototypal inheritance.
Syntactic sugar is a term for syntax changes in computer programming which make it easier for humans to code.
Javascript's fundamental data type is the object. An object is a composite value enabling aggregation of multiple values and storing and retrieval of values by name. An object is an unordered collection of properties each of which has a name and a value.
Property names are strings, which at a most basic level enable the mapping of strings to values. However, in Javascript
an object is more than sa simple string to value map, it can also maintain its own set of properties and call also inherit
properties of another object, known as a prototype
.
The methods of an object are typically inherited properties and this Prototypal Inheritance
is a key feature of Javascript.
prototypical inheritance refers to the ability to access object properties from another object.
In older versions of Javascript an object's class
attribute is string that provides information about the type of object.
However, there was no way of setting this attribute and there was only indirect technique for querying it. The code below
would return the type of class that an object has been defined as.
function classof(o) {
if (o === null) return "Null";
if (o === undefined ) return "Undefined";
return Object.prototype.toString.call(o).slice(8, -1);
}
A class is blueprint for any number of objects. A class describes the type of an object - how it is represented and what operations can be carried out on it.
The conventional wisdom in programming is that classes should be small, not deep.
A class should contain exact specifications on what can be done with an object. The specification is the class' contract.
ES 2015 introduced a special keyword class
which we can use to define a class.
class SomeCoolClass {
// The body of the class
}
The code above defines a class SomeCoolClass
. The braces { }
delimit the class body. This is an example
of the class declaration syntax.
What would Javascript be, if there weren't alternative approaches of achieving the same thing. You can also define a class
by making use of the class expression
to assign a class to a variable.
const SomeCoolClass = class {
// The body of the class
}
You can also export a class as part of ES2015 module, making use of the default export
syntax
export default class SomeCoolClass {
// The body of the class
}
The exact same thing can be achieved making use of the named export
export class SomeCoolClass {
// The body of the class
}
It is generally accepted naming convention within the Javascript community to name classes using Pascal Case in contrast to other Javascript data structures.
The constructor method is a unique method for creating and initializing the object created with the class. There can
only be one unique method with the name “constructor” in a class. The constructor can use the super
keyword to call the
constructor of the superclass, which is the constructor of the class you are deriving your class from.
A constructor enables you to provide any custom initialization that must be done before any other methods can be called on an instantiated object.
constructor(param1, param2, ...)
is a special method in the body of a class that is used to initialize an instance of a class.
It's typically the place where one would set the initial values for the fields, or do any kind of object setup.
In the example below we will add a constructor method to our class which will accept an argument for a name
value we
can add to help describe our class.
We can then initialise an instance of our class can provide some catchy name.
class SomeCoolClass {
constructor(name) {
this.name = name;
}
}
const someClass = new SomeCoolClass('A cool class name');
If you don’t define a constructor
method for a class, a default one is created. The default constructor is an
empty function, which doesn’t modify the instance.
class SomeCoolClass {
constructor() {}
}
Classes can extend - a.k.a inherit, other classes , therefore if your class is derived class, the default constructor calls the parent constructor, passing alon any arguments that were provided.
constructor(...args) {
super(...args);
}
This enables the code to function as follows.
class SomeCoolClass {
constructor(name) {
this.name = name;
}
}
class InheritedCoolClass extends SomeCoolClass {
printClassName() {
return this.name;
}
}
let cool = new InheritedCoolClass("Kajagoogoo")
console.log(`My class name is : ${ cool.printClassName() }`)
}
Class fields are variables that hold information. Fields can be attached to 2 entities:
The fields also have 2 levels of accessibility:
In the code above we created a public instance field in the constructor name
, we could access this field outside of
the class as follows
const someClass = new SomeCoolClass('A cool class name');
console.log(someClass.name); // Would print out A cool class name to the console
The downside to creating implicitly inside the constructor, as above, is it could be difficult for other developers to understand and grasp the fields list, because they would have to decipher them from the constructor’s code.
A better approach would be to explicitly declare the class fields. Therefore, irrespective of the constructor, the instance always has the same set of fields.
We can declare a name;
field inside the body of the class which declares a public field name.
The public fields declared such a way is expressive and ensures the fields exist on every created instance of the class. This ensures the field is always present and assists in ensuring the class definition is more self-documenting.
Public instance fields are added with Object.defineProperty()
either at construction time in the base class
or just after super()
returns in a subclass.
a quick look at the fields declarations is enough to understand the class’s data structure.
class SomeCoolClass {
name;
constructor(name) {
this.name = name;
}
}
There is no restriction on access or update of the public fields. Developers can read and assign values to public fields inside the constructor, methods, and outside of the class.
Developing Javascript you will often need encapsulate the inner workings or your class from the outside. Using encapsulation the public interface of the class provides does not couple it to the implementation details of the class.
encapsulation refers to the bundling of data with the methods that operate on that data, or the restricting of direct access to some of an object's components
To declare private fields in Javascript classes we only need to prefix the name with a #
symbol. The prefix should be used
everytime you declare and reference the field.
class SomeCoolClass {
#name;
constructor(name) {
this.#name = name;
}
}
The #name
is a private field and can only be accessed and modified within the body of the class. If we try access
the name
field in code now, we will now be thrown an error.
Static fields are helpful to define class constants or store information specific to the class. This is useful for caches, fixed-configuration, or any other data you don't need to be replicated across instances.
To create static fields use the special keyword static
followed by the field name: static coolStaticField
.
Although I rarely have seen this documented, a common naming convention I find in Javascript code bases is to name all static fields in Upper case.
We'll create a new field type that will indicate the type of class we're going to create, then in our sample we'll query the Static field
class Class {
static CLASS_TYPE = "Cool"
name;
type;
constructor(name, type) {
this.name = name
this.type = type
}
}
let some = new Class("Classes", Class.CLASS_TYPE)
console.log(`${ some.name } are ${ some.type }`)
The CLASS_TYPE
is a static field, therefore to access the static field you have to use the class followed by the field
name i.e. Class.CLASS_TYPE
There may be occassions when you may want to hide static fields, essentially making them private. We again
make use of the convention discussed previously and make use of the #
prefix on the field name.
Consider you would like to limit the number of instances of the Class
being created. We could hide the
details of how we implement this, by creating private static fields.
The static field Class.#MAX_INSTANCES
sets the maximum number of allowed instances, while Class.#instances
static
field counts the actual number of instances.
These private static fields are accessible only within the Class class. We cannot access or interfere externally with the limit's of this mechanism, benefiting from the encapsulation.
class Class {
static CLASS_TYPE = "Cool"
static #MAX_INSTANCES = 2
static #instances =0
name;
type;
constructor(name, type) {
this.name = name
this.type = type
Class.#instances++
if(Class.#instances > Class.#MAX_INSTANCES) {
throw new Error("Unable to create another Class Instance")
}
}
}
let some = new Class("Classes", Class.CLASS_TYPE)
let another = new Class("Another", Class.CLASS_TYPE)
let ohNo = new Class("OhNo", Class.CLASS_TYPE)
console.log(`${ some.name } are ${ some.type }`)
console.log(`${ another.name } are ${ another.type }`)
console.log(`${ ohNo.name } are ${ ohNo.type }`)
The core concept of behind classes is to provide the ability to modify data, this functionality is exposed by functions
,
this is commonly referred to in Object-Oriented Programming (OOP) languages as Methods.
Javascript supports both Instance and Static methods on classes.
Instance methods provide the ability to access and modify instance data. Instance methods can call other instance methods and static methods.
class User {
name = "Anonymous"
constructor(name) {
this.name = name;
}
userName() {
return this.name
}
}
const user = new User('Gary Woodfine')
console.log(user.userName())
userName() { ... }
is a method inside the User class, which can be accessed after the class has been initialised
using user.userName()
method invocation, which executes the method and returns the computed value.
Methods can also accept arguments and can also be private
and can provide indirect access to private static fields.
We can slightly modify the code above to provide an illustration of how we can use encapsulation usings methods and private static fields.
class User {
#name = "Anonymous"
constructor(name) {
this.#name = name;
}
userName() {
return this.#name
}
}
const user = new User('Gary Woodfine')
console.log(user.userName())
Before we provide an example of how to use arguments with methods, I want to introduce another feature of classes, which makes working with classes in Javascript really useful.
Getter and Setter fields mimic regular fields, however they provide more control in regards to how fields are accessed and changed.
Getter enable access to a field and Setter enable the setting the field value.
We can rewrite the class above to make use of Getters and Setter, which are commonly referred to in OOP as properties
class User {
#firstName = ''
#lastName = ''
constructor(firstname, lastname) {
this.#firstName = firstname
this.#lastName = lastname
}
get firstName(){
return this.#firstName
}
set firstName (firstName) {
if(firstName === '') {
throw new Error('A firstname is required')
}
this.#firstName = firstName
}
get lastName() {
return this.#lastName
}
set lastName(lastName) {
if(lastName === '') {
throw new Error('A firstname is required')
}
this.#lastName = lastName
}
get fullName {
return `${this.#firstName} ${this.#lastName}`
}
}
const user = new User('Gary', 'Woodfine')
console.log(user.userName)
console.log(`the Firstname ${user.firstName}`)
console.log(`the Lastname ${user.lastName}`)
user.firstName = 'Gareth'
console.log(user.fullName)
The get
property enable access to the value of the field: user.firstName
The set
enables updating of the field i.e. user.firstName = 'Gareth'
We also implemented a readonly
property in effect by only implementing a get property for fullName
, which simply
returns the concatenated first and last names.