Home Chapter 9: Classes.
Post
Cancel

Chapter 9: Classes.

Introduction to Classes

Classes are a template for creating objects. They encapsulate data with code to work on that data. Classes in JS are built on prototypes but also have some syntax and semantics that are not shared with ES5 class-like semantics.

Before ES6, JS used functions and prototypes to create multiple objects with similar features but different values. ES6 introduced classes to create objects using the same syntax as in other object-oriented languages like Java, C++, and Python.

Classes and Prototypes

If we define a prototype object and then use Object.create() to create a new object that inherits properties from that prototype, we can then add properties to the new object.

1
2
3
4
5
6
7
8
9
10
11
12
const Person = {
    name: "default",
    age: 0,
    greeting: function() {
        return `Hi, I'm ${this.name}`;
    }
};

const person1 = Object.create(Person);
person1.name = "John";
person1.age = 20;
console.log(person1.greeting()); // Hi, I'm John

The code above creates a prototype object called Person and then uses Object.create() to create a new object called person1 that inherits properties from Person.

The code below defines a prototype object for a class that represents a range of values and also defines a factory function that creates and initializes a new instance of the class.

1
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
function range(from, to) {
  let r = Object.create(range.methods);
  r.from = from;
  r.to = to;
  return r;
}

range.methods = {
  includes(x){
    return this.from <= x && x <= this.to;
  },

  *[Symbol.iterator]() {
    for(let x = Math.ceil(this.from); x <= this.to; x++) {
      yield x;
    }
  },

  toString() {
    return "(" + this.from + "..." + this.to + ")"
  }
}

let r = range(1, 10)
console.log(r.includes(6)) // true
console.log(r.toString()) // (1...10)
console.log([...r]); // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

The code above defines a range() factory function that creates and initializes a new object that represents a range of values. The range.methods object is the prototype object for the range instances created by the range() factory function. The range.methods object defines the methods that are inherited by range instances.

Classes and Constructors

A constructor function is a function whose name starts with a capital letter and that is intended to be used with the new prefix. The code below defines a class that represents a range of values and also defines a constructor function that initializes new instances of the class.

1
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
function Range(from, to) {
    // check if the constructor was called with new
  if (!new.target) return new Range(from, to);
    
  this.from = from;
  this.to = to;
}

Range.prototype = {
  includes(x) {
    return this.from <= x && x <= this.to;
  },

  *[Symbol.iterator]() {
    for(let x = Math.ceil(this.from); x <= this.to; x++) {
      yield x;
    }
  },

  toString() {
    return "(" + this.from + "..." + this.to + ")"
  }
}

let r = new Range(1, 10)
console.log(r.includes(6)) // true
console.log(r.toString()) // (1...10)
console.log([...r]); // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

The code above defines a Range() constructor function that initializes new objects that represent a range of values. Notice that the constructor function is defined with a capital letter and that it uses the new keyword to initialize the new object.

In the first example, we called range.methods the prototype object for the range instances created by the range() factory function. In this example, we call Range.prototype the prototype object for the range instances created by the Range() constructor function. An invocation of the Range() constructor function creates and initializes a new object that inherits properties from Range.prototype.

Another example to quench your thirst for knowledge.

1
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
function User(name, email, age, gender) {
  //check if the constructor has been initialized with the new keyword
  if(!(new.target)) return new User(name, email, age, gender)

  this.name = name
  this.email = email
  this.age = age
  this.gender = gender
}

User.prototype = {
    constructor: User, // to avoid the constructor being overwritten
  toString() {
    return `Name: ${this.name}\nEmail: ${this.email}\nAge: ${this.age}years\nGender: ${
      this.gender.toUpperCase() === 'M' ? 'Male' : this.gender.toUpperCase() === 'F' ? 'Female' : 'Not specified'
    }`;
  },
  valueOf() {
    return this.age
  },
  ageCheck() {
    let age = +this.age;
    if(age < 18)
      return "Young and requires parental guidance!"
    else if(age >= 18 && age < 35)
      return "Youthful age. All's good"
    else if(age >= 35 && age < 60)
      return "Requires filtered content for age recognition. Elders!"
    else
      return "Oops! We might not be able to render content for specified age! Sorry!"
  },
}

let user = new User("Frank", "me@mymail.com", 29, "m")
console.log(user.toString())
console.log(Number(user))
console.log(user.ageCheck())
/* Output
Name: Frank
Email: me@mymail.com
Age: 29years        
Gender: Male        
29
Youthful age. All's good
 */

Constructors, Class Identity, and instanceof

The instanceof operator tests whether an object inherits from a given prototype object. The instanceof operator can be used to test whether an object is an instance of a class.

1
2
3
let r = new Range(1, 10);
console.log(r instanceof Range); // true
console.log(r instanceof Object); // true

If you want to test the prototype chain of an object without using the instanceof operator, you can use the isPrototypeOf() method.

1
console.log(Range.prototype.isPrototypeOf(r)); // true

The constructor Property

Every function has a prototype property that refers to an object known as the prototype object. Every object has a constructor property that refers to a function known as the constructor function. The constructor property of an object is a reference to the function that was used to create the object.

1
2
3
4
let F = function() {};
let p = F.prototype;
let c = p.constructor;
console.log(c === F); // true

The constructor property of an object is not necessarily a reference to the function that created the object. The constructor property of an object is a reference to the function whose prototype property is the prototype object of the object.

1
2
let o = new F();
console.log(o.constructor === F); // true

Classes with the class Keyword

The class keyword is used to define a class. The class keyword defines a constructor function and a prototype object for the class. The class keyword is syntactic sugar for defining constructor functions and prototype objects.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Range {
  constructor(from, to) {
    this.from = from;
    this.to = to;
  }

  includes(x) {
    return this.from <= x && x <= this.to;
  }
  
  *[Symbol.iterator]() {
    for(let x = Math.ceil(this.from); x <= this.to; x++) {
      yield x;
    }
  }

  toString() {
    return "(" + this.from + "..." + this.to + ")"
  }
}

let r = new Range(1, 10)
console.log(r.includes(6)) // true
console.log(r.toString()) // (1...10)
console.log([...r]); // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

To define a class which inherits from another class, you use the extends keyword.

1
2
3
4
5
class Span extends Range {
  constructor(start, length) {
    super(start, start + length);
  }
}

Class declarations are not hoisted. They may have both expression and statement forms.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// class expression form
let Range = class {
  constructor(from, to) {
    this.from = from;
    this.to = to;
  }

  includes(x) {
    return this.from <= x && x <= this.to;
  }
  
  *[Symbol.iterator]() {
    for(let x = Math.ceil(this.from); x <= this.to; x++) {
      yield x;
    }
  }

  toString() {
    return "(" + this.from + "..." + this.to + ")"
  }
}

We have already covered class in statement form in the previous examples.

NB To summarize:

  • All code inside the class construct is automatically in strict mode.
  • Class declarations are not hoisted.

Static Methods

You can define static methods for a class by using the static keyword. Static methods are methods that are called on the class itself, not on instances of the class.

1
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
class Range {
  constructor(from, to) {
    this.from = from;
    this.to = to;
  }

  includes(x) {
    return this.from <= x && x <= this.to;
  }
  
  *[Symbol.iterator]() {
    for(let x = Math.ceil(this.from); x <= this.to; x++) {
      yield x;
    }
  }

  toString() {
    return "(" + this.from + "..." + this.to + ")"
  }

  static parse(s) {
    let matches = s.match(/^\((\d+)\.\.\.(\d+)\)$/);
    if(!matches) {
      throw new TypeError(`Cannot parse Range from "${s}".`);
    }
    return new Range(parseInt(matches[1]), parseInt(matches[2]));
  }
}

let r = Range.parse('(1...10)');
console.log(r.includes(6)); // true
console.log(r.toString()); // (1...10)
console.log([...r]); // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

Takeaway👉 Static methods are also known as class methods. This is because they are called on the class itself, not on instances of the class. Do not use this in static methods.

Getters and Setters

You can define getters and setters for a class by using the get and set keywords.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Person {
  constructor(name) {
    this.name = name;
  }

  get name() {
    return this.name;
  }

  set name(value) {
    if(value.length < 4) {
      console.log("Name is too short.");
      return;
    }
    this.name = value;
  }
}

let p = new Person("Frank");
console.log(p.name); // Frank
p.name = "John";
console.log(p.name); // John
p.name = "Joe";
console.log(p.name); // Name is too short.

Public, Private and Static Fields

You can define public, private and static fields for a class by using the # and static keywords.

1
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
class Person {
  #name;

  constructor(name) {
    this.#name = name;
  }

  get name() {
    return this.#name;
  }

  set name(value) {
    if(value.length < 4) {
      console.log("Name is too short.");
      return;
    }
    this.#name = value;
  }

  static #age = 29;

  static get age() {
    return Person.#age;
  }

  static set age(value) {
    if(value < 18) {
      console.log("Too young.");
      return;
    }
    Person.#age = value;
  }
}

let p = new Person("Frank");
console.log(p.name); // Frank
p.name = "John";
console.log(p.name); // John
p.name = "Joe";
console.log(p.name); // Name is too short.
console.log(Person.age); // 29
Person.age = 17; // Too young.
Person.age = 30;
console.log(Person.age); // 30

Below is a concrete, tested example that implements instance methods, static methods, getters, setters, public fields, private fields, public methods, private methods, static fields, static methods, and inheritance.

1
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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
class CylindricalContainer {
  #radius = 0; // private field
  #height=0;
  #diameter = this.#radius * 2;
  static #description = "This is all about the different computations to be applied on a cylinder!"; // private static field

  constructor(radius, height) {
    this.#radius = radius;
    this.#height = height;
    this.#diameter = this.#radius * 2;
  }

  static get description() {
    return CylindricalContainer.#description // private static field
  }

  get radius() {return this.#radius}
  set radius(r) {this.#radius = r}

  get diameter() { return this.#diameter }

  get height() {return this.#height}
  set height(l) {this.#height = l}

  differenceInMeasurements(that) {
    return new CylindricalContainer(Math.abs(this.#radius - that.#radius), Math.abs(this.#height - that.#height))
  }

  static difference(a, b) {return a.differenceInMeasurements(b)}

  equals(that) {
    return that instanceof CylindricalContainer && this.#radius === that.#radius && this.#height === that.#height
  }

  toString() {
    return `Radius: ${this.radius}\nLength: ${this.height}`
  }
}

// Adding methods to existing classes
CylindricalContainer.prototype.surfaceArea = (function() { // IIFE to avoid polluting the global scope
  return (function(openOrClosedOrTwoSidesOpen) { 
    console.log(`Is CylindricalContainer this : ${this instanceof CylindricalContainer}`)
    let circleArea = Math.PI * (this.radius ** 2);
    let curvedSurface = Math.PI * this.diameter * this.height;
    let formulaOpen = circleArea + curvedSurface
    let formulaClose = (2 * circleArea) +  curvedSurface
    switch(openOrClosedOrTwoSidesOpen) {
      case 'o':
        return Math.ceil(formulaOpen)
      case 'c':
        return Math.ceil(formulaClose)
      case 'b':
        return Math.ceil(curvedSurface)
      default:
        return "Case unknown! Please use 'o' for open on one circular base, 'b' for both open circular bases and 'c' for closed circular bases"
    }
  });
})();

console.log(CylindricalContainer.description)
let cylinder = new CylindricalContainer(7, 10);
console.log(cylinder.height);
console.log(cylinder.radius);
console.log('The surface area of the cylinder is : ' + cylinder.surfaceArea('o') + ' units squared.');
let cylinder2 = new CylindricalContainer(14, 30);
console.log(CylindricalContainer.difference(cylinder, cylinder2).toString());
console.log(cylinder.equals(cylinder2));

/* Output
This is all about the different computations to be applied on a cylinder!
10                                                      
7                                                       
Is CylindricalContainer this : true                     
The surface area of the cylinder is : 594 units squared.
Radius: 7                                               
Length: 20                                              
false                                                  
*/

Subclassing with extends

You can create a subclass of a class by using the extends keyword. When you extend a class, the subclass inherits all the methods and properties of the parent class. To call the constructor of the parent class, use the super() method. This is useful for setting inherited properties that require access to the parent class’s methods. It must be called before you can use the this keyword in the constructor.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Person {
  constructor(name) {
    this.name = name;
  }

  sayHi() {
    console.log(`Hi, I'm ${this.name}.`);
  }
}

class Employee extends Person {
  constructor(name, title) {
    super(name); // call the super class constructor and pass in the name parameter
    this.title = title;
  }

  sayHi() {
    super.sayHi(); // call the super class method and pass in the name parameter. It will print "Hi, I'm Francis."
    console.log(`I'm a ${this.title}.`); 
  }
}

let e = new Employee("Francis", "JSWiz");
e.sayHi(); // Hi, I'm Francis. I'm a JSWiz.

A better example is given below…

1
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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
class Vehicle {
  static #description = "This is all about effectiveness of classes and subclassing";

  #name = "";
  constructor() {
    this.#name = "";
  }

  static get description() { return Vehicle.#description }
  get name() { return this.#name }
  set name(nameDecl) { this.#name = nameDecl }
  toString() {
    return this.name;
  }
}

class Car extends Vehicle { // Car is a subclass of Vehicle

  #data = [];

  get dataArray() { return this.#data }

  constructor(arrayData, start=0, end=Array.isArray(arrayData) ? arrayData.length -1 : start) {
      // check if arrayData is an array
    if(!Array.isArray(arrayData)) throw new TypeError("Expected \x1b[35m Array \x1b[0m: Found \x1b[32m" + typeof arrayData + "\x1b[0m");
    //loop through array
    super("Luke");
    this.start = start;
    this.end = end;
    this.name = ""
    this.year = 0
    this.sellingPrice = 0
    this.kmDriven = 0
    this.fuel = 0
    this.sellerType = ""
    this.transmission = ""
    this.owner = ""
    for(let i = start; i <= end; this.#data.push(arrayData[i]), i++) /*Empty*/;
  }

  *[Symbol.iterator]() {
    console.log(`Length of items: ${this.#data.length}`);
    for (let x = 0; x < this.#data.length; x++) {
      console.log("Entry no." + (x+1));
      let str = "|";
      for(let [key, value] of Object.entries(this.#data[x])) {
        if(key === "name")
          str += `${key} : ${value.toString().padEnd(10) } |`
        else
          str += `${key} : ${value.toString().padStart(2) } |`
      }
      console.log(str);
    }
  }
}

// contains an array of objects - Removed most of the objects for page span purposes.... Output still remains okay when testing
const data = [
  {
    "name": "Chevrolet Tavera Neo 3 LS 7 C BSIII",
    "year": 2015,
    "selling_price": 400000,
    "km_driven": 120000,
    "fuel": "Diesel",
    "seller_type": "Individual",
    "transmission": "Manual",
    "owner": "First Owner"
  },
  {
    "name": "Hyundai EON Magna Plus",
    "year": 2012,
    "selling_price": 170000,
    "km_driven": 60000,
    "fuel": "Petrol",
    "seller_type": "Individual",
    "transmission": "Manual",
    "owner": "First Owner"
  },
  {
    "name": "Honda Brio E MT",
    "year": 2013,
    "selling_price": 260000,
    "km_driven": 70000,
    "fuel": "Petrol",
    "seller_type": "Individual",
    "transmission": "Manual",
    "owner": "Second Owner"
  },
  {
    "name": "Maruti Swift VVT ZXI",
    "year": 2017,
    "selling_price": 550000,
    "km_driven": 60000,
    "fuel": "Petrol",
    "seller_type": "Individual",
    "transmission": "Manual",
    "owner": "First Owner"
  }]

let i = new Car(data, 0, 8);
console.log(Vehicle.description); 
[...i]; // Iterate through the array of objects

/*Output
This is all about effectiveness of classes and subclassing
Length of items: 9
Entry no.1
|name : Ford EcoSport 1.5 Diesel Titanium Plus BSIV |year : 2017 |selling_price : 840000 |km_driven : 49213 |fuel : Dies
el |seller_type : Dealer |transmission : Manual |owner : First Owner |
Entry no.2
|name : Hyundai i10 Sportz 1.1L |year : 2015 |selling_price : 229999 |km_driven : 40000 |fuel : Petrol |seller_type : In
dividual |transmission : Manual |owner : First Owner |
...(remaining items)
 */

Composition

Composition is a way to combine simple objects or data types into more complex ones. For example, a Person class may have a name property, which is a String. A Person may also have an address property, which is a String. However, a Person is not a String, and a String is not a Person. Instead, a Person has a String as a property. This is composition.

1
2
3
4
5
6
7
8
9
10
class Person {
  constructor(name, address) {
    this.name = name;
    this.address = address;
  }
}

let p = new Person("Francis", "123 Main St.");
console.log(p.name); // Francis
console.log(p.address); // 123 Main St.

Inheritance vs. Composition

Inheritance is a way to create a class as a specialization of another class. Composition is a way to combine simple objects or data types into more complex ones. Inheritance is a “is-a” relationship. Composition is a “has-a” relationship.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Person {
  constructor(name) {
    this.name = name;
  }
}

class Employee extends Person {
  constructor(name, title) {
    super(name);
    this.title = title;
  }
}

let e = new Employee("Francis", "JSWiz");
console.log(e.name); // Francis
console.log(e.title); // JSWiz

The Employee class is a Person, and a Person is an Employee. This is inheritance.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Person {
  constructor(name, address) {
    this.name = name;
    this.address = address;
  }
}

class Employee {
  constructor(person, title) {
    this.person = person;
    this.title = title;
  }
}

let p = new Person("Francis", "123 Main St.");
let e = new Employee(p, "JSWiz");
console.log(e.person.name); // Francis
console.log(e.person.address); // 123 Main St.
console.log(e.title); // JSWiz

The Employee class has a Person as a property and a title as a property. The Employee class is not a Person, and a Person is not an Employee. Instead, an Employee has a Person as a property. This is composition.

Preferring composition to inheritance is a common design pattern in object-oriented programming. It is often more flexible and easier to maintain. It is also more natural to think about objects in terms of what they have rather than what they are.

Class Hierarchies and Abstract Classes

A class hierarchy is a way to organize classes into a tree structure. The root of the tree is the base class. The leaves of the tree are the derived classes. Each derived class inherits from the base class. Each derived class may also have its own derived classes. This is called a class hierarchy.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Person {
  constructor(name) {
    this.name = name;
  }
}

class Employee extends Person {
  constructor(name, title) {
    super(name);
    this.title = title;
  }
}

class Manager extends Employee {
  constructor(name, title, department) {
    super(name, title);
    this.department = department;
  }
}

let m = new Manager("Francis", "JSWiz", "IT");
console.log(m.name); // Francis
console.log(m.title); // JSWiz
console.log(m.department); // IT

The Manager class is a Employee, and a Employee is a Person.

An abstract class is a class that cannot be instantiated. It is used as a base class for other classes. It is also used to define a common interface for a set of derived classes. It is a way to define a class hierarchy without having to implement all the details. Here’s an example of how you can create an abstract class using the class keyword in JavaScript:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class AbstractClass {
  constructor() {
    if (new.target === AbstractClass) {
      throw new TypeError("Cannot instantiate abstract class");
    }
  }

  // Abstract method
  abstractMethod() {
    throw new Error("Abstract method must be implemented");
  }

  // Non-abstract method
  regularMethod() {
    console.log("This is a regular method");
  }
}

In the above example, the AbstractClass serves as the abstract class. It contains a constructor that throws an error if an attempt is made to directly instantiate the abstract class (new.target is a meta-property that refers to the constructor that was directly invoked with the new keyword).

The abstractMethod() is declared without an implementation. It throws an error, indicating that any derived classes must provide an implementation for this method. On the other hand, the regularMethod() is a non-abstract method that has an implementation and can be used directly.

To create a derived class from the abstract class, you can extend the AbstractClass and provide the implementation for the abstract method:

1
2
3
4
5
6
7
8
9
class ConcreteClass extends AbstractClass {
  abstractMethod() {
    console.log("Implementation of abstractMethod");
  }
}

const instance = new ConcreteClass();
instance.abstractMethod(); // Output: "Implementation of abstractMethod"
instance.regularMethod();  // Output: "This is a regular method"

In this example, the ConcreteClass extends the AbstractClass and provides an implementation for the abstractMethod(). The ConcreteClass can now be instantiated, and the implementation of the abstract method can be called. If you try to instantiate the AbstractClass directly, you will get an error:

1
const instance = new AbstractClass(); // Error: "Cannot instantiate abstract class"

This brings us to the end of this lesson. Here’s a quick recap of what we covered:

  • Classes are a way to create objects with properties and methods.
  • Classes are defined using the class keyword.
  • Classes can have a constructor method that is used to initialize the object.
  • Classes can have methods that are used to perform actions on the object.
  • Classes can have properties that are used to store data on the object.
  • Classes can have getters and setters that are used to get and set the values of properties.
  • Classes can have static methods that are used to perform actions on the class.
  • Classes can have static properties that are used to store data on the class.
  • Classes can be extended to create a subclass.
  • Classes can be composed to create a more complex class.
  • Classes can be used to create a class hierarchy.
  • Classes can be used to create an abstract class.

🚀 Challenge

Create a class hierarchy for a zoo. The zoo has animals, and animals have names and ages. The zoo has a zookeeper, and the zookeeper has a name and a list of animals that they take care of. The zookeeper can feed the animals, and the animals can make sounds.

Make sure to create a class hierarchy that uses inheritance and composition.

Solution

1
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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
class Zoo {
  constructor() {
    this.animals = [];
    this.zookeeper = new Zookeeper("Francis");
  }

  addAnimal(animal) {
    this.animals.push(animal);
    this.zookeeper.addAnimal(animal);
  }
}

class Animal {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }

  makeSound() {
    console.log("Generic animal sound");
  }
}

class Zookeeper {
  constructor(name) {
    this.name = name;
    this.animals = [];
  }

  addAnimal(animal) {
    this.animals.push(animal);
  }

  feedAnimals() {
    for (let animal of this.animals) {
      console.log(`${this.name} is feeding ${animal.name}`);
    }
  }
}

class Lion extends Animal {
  constructor(name, age) {
    super(name, age);
  }

  makeSound() {
    console.log("Roar!");
  }
}

class Tiger extends Animal {
  constructor(name, age) {
    super(name, age);
  }

  makeSound() {
    console.log("Rawr!");
  }
}

class Bear extends Animal {
  constructor(name, age) {
    super(name, age);
  }

  makeSound() {
    console.log("Grrr!");
  }
}

let zoo = new Zoo();
let lion = new Lion("Simba", 3);
let tiger = new Tiger("Tigger", 5);
let bear = new Bear("Winnie", 10);
zoo.addAnimal(lion);
zoo.addAnimal(tiger);
zoo.addAnimal(bear);
zoo.zookeeper.feedAnimals();
lion.makeSound();
tiger.makeSound();
bear.makeSound();
Made with ❤ by LucasGithub.Twitter
This post is licensed under CC BY 4.0 by the author.