JS Prototypes, Classes, and Modules
# Base Classes
When you build a single JS class with no extend keyword, you are building a base class. A base class is like a blueprint that defines generic information about the type of object you want to create.
As an example, let's say you were going to build a forest. It will be filled with plants and animals and fungi. As base classes you would create class Plant, class Animal, and class Fungi. These
are the most generic object types that we can define.
These classes will only contain properties and methods that are common to all other classes that are derived from them.
Any class can be a base class. It defines a relative position. Compared to other classes is can be the base class.
# Sub and Super Classes
When you want to build a new type of object that is a more specific, or more narrowly focused version of some other class, then the other class is the base class and the new more specific type is
the sub class. When you define the sub class, it uses the keyword extends to connect the base and sub classes.
A base class can also be called a super class or parent class. Generally speaking the base would be the first class in a chain of classes.
Using Tree, OakTree, and WhiteOakTree as example object types we would write the classes like this:
class Tree {
//defines all the properties and methods for any kind of tree
constructor() {
//constructor for the Tree class
}
}
class OakTree extends Tree {
constructor() {
super(); //call the constructor inside Tree
this.type = 'Oak';
this.seed = 'Acorn';
}
}
class WhiteOakTree extends OakTree {
constructor() {
super(); //call the constructor inside OakTree
this.variety = 'WhiteOak';
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
The sub class is the one with extends in the first line. The base class, or parent or super class, is the class name that comes after the keyword extends.
In the above example Tree is the base class, and both OakTree and WhiteOakTree are parent or super classes.
The difference between the two constructor methods is that the one in the sub class needs to start with the method super(), which will call the constructor inside the super class.
# Person and Robot Class
To further illustrate how multiple sub and super classes can interact, here are a couple base classes for Person and Robot.
class Person {
#firstName; //a private instance property
#lastName; //a private instance property
static species = 'Homo Sapien'; //a public static property
constructor(_first, _last) {
this.#firstName = _first;
this.#lastName = _last;
}
get name() {
//a public instance getter method
return `${this.#firstName} ${this.#lastName}`;
}
talk(words, element) {
//a public instance method
//element is the elementReference or id of an element
console.log(words);
let ref;
if (typeof element == 'string') {
ref = document.querySelector(element);
} else {
if (typeof HTMLElement != 'undefined' && element instanceof HTMLElement) {
ref = element;
}
}
if (ref) ref.textContent = words;
}
}
class Robot {
#_color; //a private instance member
#isShiny = false; //a private instance member
purpose = ''; //a public instance member
constructor(_color, _isShiny, _purpose) {
this.#_color = _color;
this.#isShiny = _isShiny;
this.purpose = _purpose;
}
get color() {
//a public getter instance method
let str = `The robot is ${this.#_color}`;
str += this.#isShiny ? ' and shiny.' : ' but not shiny.';
return str;
}
talk(element) {
//a public instance method
//element is the elementReference or id of an element
console.log('Beep');
let ref;
if (typeof element == 'string') {
ref = document.querySelector(element);
} else if (typeof HTMLElement != 'undefined' && element instanceof HTMLElement) {
ref = element;
} else {
console.log('Beep');
}
if (ref) ref.textContent = 'Beep';
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
# Extending Person Class
From the generic base Person class we can build a Professor class as a more specific type of Person object. Then we can create a Adesh class, which is a more specific type of Professor object.
We are creating an inheritance chain Person -> Professor -> Adesh. In Javascript terms what we have done is create a prototype chain where we can call new Person() to create an object of type
Person, new Professor() to create an object of type Professor, or new Adesh() to create an object of type Adesh.
Each of the three constructor functions Person, Professor, and Adesh will have an attached prototype object. This prototype object is where all the methods that are shared by all instances are
held. For example, both the Person and Robot classes have a talk instance method. These methods are kept in Person.prototype.talk and Robot.prototype.talk.
The prototype chain connecting all the possible instance methods is created by us using the keyword extends. Adesh.prototype is connected to Professor.prototype which is connected to
Person.prototype.
The Person object not extending anything else explicitly means that Person.prototype will be connected to Object.prototype. That is the top prototype object. Above that is only null.
class Professor extends Person {
constructor(_first, _last, _subject) {
super(_first, _last);
this.subjectArea = _subject;
}
}
class Adesh extends Professor {
constructor(_last) {
super('Adesh', _last, 'Data Science');
}
//a static method
static isA() {
return 'an Adesh';
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Notice how each call to super() passes the parameters required by the constructor in the parent class.
When we create a new instance of the Adesh object, it will "inherit" through its prototype chain, all the properties and methods from its parent classes.
# Instance methods and properties
Every instance method and property is attached to an instance of a class. For example, the adesh variable below holds reference to an instance of the Adesh class.
let adesh = new Adesh('Shah');
//create an instance of the Adesh class by calling the constructor function
console.log(adesh.subjectArea); //Data Science
//accesses the subjectArea property from the Professor class
console.log(adesh.name); // Adesh Shah
//accesses the name getter method from the Person class
adesh.talk('MAD&D is a great program.');
//calls the talk method from the Person class
2
3
4
5
6
7
8
# static methods and properties
Static methods and properties are not connected to a specific instance. They are connected directly to the class object. The value of a static property is shared across all instances. If it is
changed, then it is changed for all of them.
To call a static method, you prefix it with the name of the class, not a variable name.
In the Person class there is a static species property
let adesh = new Adesh('Shah');
//adesh is an instance of the Adesh class.
console.log(adesh.name); // Adesh Shah
//access the instance getter method from the Person class
console.log(Person.species); // Homo Sapien
//access the static method from the Adesh class
console.log('adesh is', Adesh.isA()); // adesh is an Adesh
2
3
4
5
6
7
How Many Classes?
When designing and building classes, there is no strict rules that will tell you how many classes you need or which properties and methods belong in each class. These are design decisions. You make the most logical decisions that you can in the hopes of making the code easier for you and other developers to follow and use later on.
# Extending Robot Class
The same approach will work for extending the Robot class.
class HumanoidRobot extends Robot {
this.holding = null;
static numberBuilt = 0;
constructor(_color){
super(_color, false, 'replicate human skills');
HumanoidRobot.numberBuilt++;
}
pickUpTool(_toolType){
this.holding = _toolType;
}
dropTool(){
this.holding = null;
}
useTool(){
if(this.holding) return `Using ${this.holding}`;
return 'Not currently holding a tool';
}
}
class SoldierBot extends HumanoidRobot {
this.#weaponList = [];
static #secretKey = 'AdminSecret1234';
static numberBuilt = 0;
constructor(_weapons){
super('black');
this.#weaponList = _weapons;
this.#serialNumber = crypto.randomUUID();
SoldierBot.numberBuilt++;
}
addWeapon(_weapon){
this.#weaponList.push(_weapon);
return this.#weaponList.length;
}
showWeaponList(pwd){
if(pwd === SoldierBot.#secretKey){
return this.#weaponList;
}else{
return 'Access Denied';
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
The prototype chain here is Robot -> HumanoidRobot -> SoldierBot.
The HumanoidRobot only requires one parameter a color, but it needs to pass three parameters to super(). When the SoliderBot is instantiated it takes an Array as its only parameter for the
constructor and then passes a different single parameter to it's super() call.
let weapons = ['M16', 'Colt M1911', 'M67 Fragmentation grenade'];
let ranger1 = new SoldierBot(weapons);
console.log(SoldierBot.numberBuilt); // 1
console.log(ranger1.color); // black
//once the array is added to the SoldierBot instance it is hidden in a private instance property
//we can add another item using the instance method
ranger1.addWeapon('M39'); // 4
//to retrieve the whole list of weapons we need to use the instance method
//but it requires a value that matches the value of the static private property secretKey
ranger1.showWeaponList('guessing'); // Access Denied
ranger1.showWeaponList('AdminSecret1234'); // ['M16', 'Colt M1911', 'M67 Fragmentation grenade', 'M39']
console.log(HumanoidRobot.numberBuilt); // 1 - because a SoliderBot was instantiated and called super()
console.log(ranger1.weaponList); // undefined. it is a private property
2
3
4
5
6
7
8
9
10
11
12
13
# Practice
To practice and be sure that you understand how this works, start with the following class as your base class.
class Fish {
#_color;
#_numFins;
#_numGills; //private so cannot be changed externally
static #_numFish = 0;
constructor(_color, _numFins = 3, _numGills = 4) {
this.#_color = _color;
this.#_numFins = _numFins;
this.#_numGills = _numGills;
Fish.#_numFish++; //increment total number of fish
}
get totalFish() {
return Fish.#_numFish;
//expose the value of the static private _numFish property
}
get color() {
return this.#_color;
//using getters to provide the private values
}
get numberOfFins() {
return this.#_numFins;
}
get numberOfGills() {
return this.#_numGills;
}
swim(_speed) {
if (isNaN(_speed)) throw Error('Speed must be numeric');
let output = Array(_speed).fill('Swish').join(', ');
console.log(output);
return output;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
Now, create two more classes to create the prototype chain Fish -> Shark -> TigerShark.
Try to add a private Boolean property to the Shark class indicating if it is a man-eater type of shark. This value will need to be set through the constructor.
In the TigerShark class it will need to call super() from its constructor and pass a true value for the man-eater property in the Shark class.
Both the Shark and TigerShark classes should have a static property to track the number of each of these classes have been instantiated. Keep the value private and create a getter method to expose the value.
Add some other public instance method of your own to each of the two classes.
Run your code and see if you can instantiate each of the classes and call all the methods and access any of the public properties.