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();