JavaScript and TypeScript course Β· Module 2: Functions, Arrays and Objects
Prototypes and Prototypal Inheritance
In this lesson12
In Jurassic Park, managing the different dinosaur species effectively requires understanding the similarities between them and the ways traits can be inherited. JavaScript uses prototypes to implement inheritance - this is one of the most unique and powerful aspects of the language. Understanding prototypes and prototypal inheritance is key to writing efficient, flexible JavaScript code.
What Are Prototypes?
In JavaScript every object has an internal property called its prototype, which is a reference to another object. When we try to access a property of an object, JavaScript first checks whether the object itself has that property. If not, it checks the object's prototype, then the prototype's prototype, and so on, forming what is known as the prototype chain.
1// We create a simple object
2const dinosaur = {
3 species: "Tyrannosaurus",
4 makesSound: true,
5 roar() {
6 return "ROOOOAR!";
7 }
8};
9
10// We create a new object that inherits from the dinosaur object
11const rex = Object.create(dinosaur);
12rex.name = "Rex";
13rex.age = 8;
14
15// rex has access to its own properties
16console.log(rex.name); // "Rex"
17console.log(rex.age); // 8
18
19// rex also has access to the properties and methods of dinosaur
20console.log(rex.species); // "Tyrannosaurus"
21console.log(rex.makesSound); // true
22console.log(rex.roar()); // "ROOOOAR!"
23
24// Let's check whether rex has its own 'species' property (it does not)
25console.log(rex.hasOwnProperty('species')); // false
26
27// But it does have its own 'name' property
28console.log(rex.hasOwnProperty('name')); // trueIn the example above, rex has no species property of its own, but accessing rex.species works, because JavaScript looks for that property in the prototype of rex, which is the dinosaur object.
The Prototype Chain
Prototypes form a chain that JavaScript searches when we try to access a property. We call this mechanism the prototype chain.
1// The basic ancestor object
2const animal = {
3 isAlive: true,
4 eat() {
5 return `${this.name} is eating.`;
6 }
7};
8
9// A dinosaur object inheriting from animal
10const dinosaur = Object.create(animal);
11dinosaur.isExtinct = false;
12dinosaur.roar = function() {
13 return `${this.name} roars!`;
14};
15
16// A specific dinosaur species inheriting from dinosaur
17const tyrannosaurus = Object.create(dinosaur);
18tyrannosaurus.name = "Tyrannosaurus";
19tyrannosaurus.diet = "carnivore";
20tyrannosaurus.length = 12; // meters
21
22// tyrannosaurus has access to the properties of all the prototypes in the chain
23console.log(tyrannosaurus.name); // "Tyrannosaurus" (own property)
24console.log(tyrannosaurus.isExtinct); // false (from dinosaur)
25console.log(tyrannosaurus.isAlive); // true (from animal)
26console.log(tyrannosaurus.eat()); // "Tyrannosaurus is eating." (method from animal, this refers to tyrannosaurus)
27console.log(tyrannosaurus.roar()); // "Tyrannosaurus roars!" (method from dinosaur)Constructors and Prototypes
In practice, prototypes are most often used with constructors and classes to implement inheritance in JavaScript.
Constructor Functions and Prototypes
Every constructor function has a prototype property, which is used as the prototype for objects created with that function and the new operator.
1// Constructor function for Dinosaur
2function Dinosaur(name, species, diet) {
3 this.name = name;
4 this.species = species;
5 this.diet = diet;
6 this.isExtinct = false;
7}
8
9// We add methods to the Dinosaur prototype
10Dinosaur.prototype.roar = function() {
11 const volume = this.diet === "carnivore" ? "loudly" : "quietly";
12 return `${this.name} roars ${volume}!`;
13};
14
15Dinosaur.prototype.describe = function() {
16 return `${this.name} is a ${this.species} that is ${this.diet === "carnivore" ? "carnivorous" : "herbivorous"}.`;
17};
18
19// We create instances
20const trex = new Dinosaur("Rex", "Tyrannosaurus", "carnivore");
21const tricera = new Dinosaur("Trixie", "Triceratops", "herbivore");
22
23console.log(trex.roar()); // "Rex roars loudly!"
24console.log(tricera.roar()); // "Trixie roars quietly!"
25console.log(trex.describe()); // "Rex is a Tyrannosaurus that is carnivorous."
26
27// All instances share the same prototype methods
28console.log(trex.roar === tricera.roar); // trueIn this example, the roar and describe methods are defined on the prototype of the Dinosaur constructor function. All instances created with new Dinosaur() share the same methods, which is much more efficient than defining these methods in the constructor separately for every instance.
Checking an Object's Prototype
We can check an object's prototype in several ways:
1// Using Object.getPrototypeOf()
2console.log(Object.getPrototypeOf(trex) === Dinosaur.prototype); // true
3
4// Using __proto__ (a deprecated property, not recommended in production code)
5console.log(trex.__proto__ === Dinosaur.prototype); // true
6
7// Checking whether an object is an instance of a constructor
8console.log(trex instanceof Dinosaur); // true
9console.log(trex instanceof Object); // true (all objects inherit from Object.prototype)Inheritance Between Constructors
We can implement inheritance between constructor functions by linking their prototypes:
1// Constructor function for Animal (base class)
2function Animal(name) {
3 this.name = name;
4 this.isAlive = true;
5}
6
7Animal.prototype.eat = function(food) {
8 return `${this.name} eats ${food}.`;
9};
10
11Animal.prototype.sleep = function() {
12 return `${this.name} is sleeping.`;
13};
14
15// Constructor function for Dinosaur (derived class)
16function Dinosaur(name, species, diet) {
17 // We call the base class constructor
18 Animal.call(this, name);
19
20 // We add properties specific to Dinosaur
21 this.species = species;
22 this.diet = diet;
23 this.isExtinct = false;
24}
25
26// We set the Dinosaur prototype so that it inherits from Animal.prototype
27Dinosaur.prototype = Object.create(Animal.prototype);
28
29// We restore the correct constructor property
30Dinosaur.prototype.constructor = Dinosaur;
31
32// We add methods specific to Dinosaur
33Dinosaur.prototype.roar = function() {
34 const volume = this.diet === "carnivore" ? "loudly" : "quietly";
35 return `${this.name} roars ${volume}!`;
36};
37
38// We create a Dinosaur instance
39const velociraptor = new Dinosaur("Blue", "Velociraptor", "carnivore");
40
41// We have access to the methods from Animal
42console.log(velociraptor.eat("meat")); // "Blue eats meat."
43console.log(velociraptor.sleep()); // "Blue is sleeping."
44
45// And to the methods specific to Dinosaur
46console.log(velociraptor.roar()); // "Blue roars loudly!"
47
48// We check the prototype chain
49console.log(velociraptor instanceof Dinosaur); // true
50console.log(velociraptor instanceof Animal); // true
51console.log(velociraptor instanceof Object); // trueThis pattern for implementing inheritance between constructors is often called "pseudoclassical inheritance", because it emulates class-based inheritance in a language that was originally based on prototypes.
Multi-Level Inheritance
We can build multi-level inheritance hierarchies by linking prototype chains:
1// Animal base class
2function Animal(name) {
3 this.name = name;
4 this.isAlive = true;
5}
6
7Animal.prototype.breathe = function() {
8 return `${this.name} is breathing.`;
9};
10
11// Dinosaur class inheriting from Animal
12function Dinosaur(name, period) {
13 Animal.call(this, name);
14 this.period = period;
15 this.extinct = true;
16}
17
18Dinosaur.prototype = Object.create(Animal.prototype);
19Dinosaur.prototype.constructor = Dinosaur;
20
21Dinosaur.prototype.roar = function() {
22 return `${this.name} roars!`;
23};
24
25// Theropod class inheriting from Dinosaur
26function Theropod(name, period, speed) {
27 Dinosaur.call(this, name, period);
28 this.speed = speed; // km/h
29 this.bipedal = true;
30}
31
32Theropod.prototype = Object.create(Dinosaur.prototype);
33Theropod.prototype.constructor = Theropod;
34
35Theropod.prototype.run = function() {
36 return `${this.name} runs at a speed of ${this.speed} km/h!`;
37};
38
39// Tyrannosaurus class inheriting from Theropod
40function Tyrannosaurus(name, size) {
41 Theropod.call(this, name, "Late Cretaceous", 30);
42 this.size = size;
43 this.teeth = 60;
44}
45
46Tyrannosaurus.prototype = Object.create(Theropod.prototype);
47Tyrannosaurus.prototype.constructor = Tyrannosaurus;
48
49Tyrannosaurus.prototype.bite = function() {
50 return `${this.name} bites with enormous force thanks to its ${this.teeth} teeth!`;
51};
52
53// We create a Tyrannosaurus instance
54const rex = new Tyrannosaurus("Rex", "very large");
55
56// We have access to the methods of all the prototypes in the chain
57console.log(rex.breathe()); // "Rex is breathing." (from Animal)
58console.log(rex.roar()); // "Rex roars!" (from Dinosaur)
59console.log(rex.run()); // "Rex runs at a speed of 30 km/h!" (from Theropod)
60console.log(rex.bite()); // "Rex bites with enormous force thanks to its 60 teeth!" (from Tyrannosaurus)
61
62// We check the prototype chain
63console.log(rex instanceof Tyrannosaurus); // true
64console.log(rex instanceof Theropod); // true
65console.log(rex instanceof Dinosaur); // true
66console.log(rex instanceof Animal); // true
67console.log(rex instanceof Object); // truePrototypes and Classes in ES6+
ES6 introduced the class syntax, which uses prototypes under the hood but offers a friendlier syntax, similar to classical object-oriented programming:
1// Animal base class
2class Animal {
3 constructor(name) {
4 this.name = name;
5 this.isAlive = true;
6 }
7
8 eat(food) {
9 return `${this.name} eats ${food}.`;
10 }
11
12 sleep() {
13 return `${this.name} is sleeping.`;
14 }
15}
16
17// Dinosaur class inheriting from Animal
18class Dinosaur extends Animal {
19 constructor(name, species, diet) {
20 super(name); // Calling the base class constructor
21 this.species = species;
22 this.diet = diet;
23 this.isExtinct = false;
24 }
25
26 roar() {
27 const volume = this.diet === "carnivore" ? "loudly" : "quietly";
28 return `${this.name} roars ${volume}!`;
29 }
30
31 // We override the eat method from the base class
32 eat(food) {
33 if (this.diet === "carnivore" && food === "meat") {
34 return `${this.name} hungrily devours ${food}!`;
35 } else if (this.diet === "herbivore" && food === "plants") {
36 return `${this.name} calmly nibbles on ${food}.`;
37 } else {
38 return `${this.name} is not interested in eating ${food}.`;
39 }
40 }
41}
42
43// Velociraptor derived class inheriting from Dinosaur
44class Velociraptor extends Dinosaur {
45 constructor(name, speed) {
46 super(name, "Velociraptor", "carnivore");
47 this.speed = speed;
48 this.packHunter = true;
49 }
50
51 hunt() {
52 return `${this.name} hunts in a pack at a speed of ${this.speed} km/h!`;
53 }
54}
55
56// We create instances
57const blue = new Velociraptor("Blue", 70);
58
59console.log(blue.eat("meat")); // "Blue hungrily devours meat!"
60console.log(blue.sleep()); // "Blue is sleeping."
61console.log(blue.roar()); // "Blue roars loudly!"
62console.log(blue.hunt()); // "Blue hunts in a pack at a speed of 70 km/h!"
63
64// We check the prototype chain
65console.log(blue instanceof Velociraptor); // true
66console.log(blue instanceof Dinosaur); // true
67console.log(blue instanceof Animal); // true
68console.log(blue instanceof Object); // trueThe class syntax in ES6 is just "syntactic sugar" over the existing prototype mechanism in JavaScript. Under the hood, JavaScript still uses prototypes to implement inheritance.
Multiple Inheritance and Mixins
JavaScript does not directly support multiple inheritance (inheriting from several classes at once), but we can use the "mixins" pattern to achieve a similar effect:
1// Base class
2class Dinosaur {
3 constructor(name, species) {
4 this.name = name;
5 this.species = species;
6 }
7
8 roar() {
9 return `${this.name} roars!`;
10 }
11}
12
13// Mixin for carnivore behavior
14const CarnivoreMixin = {
15 hunt(prey) {
16 return `${this.name} hunts ${prey}!`;
17 },
18
19 bite() {
20 return `${this.name} bites with great force!`;
21 }
22};
23
24// Mixin for pack hunting behavior
25const PackHunterMixin = {
26 coordinateAttack() {
27 return `${this.name} coordinates the pack's attack!`;
28 },
29
30 communicateWithPack() {
31 return `${this.name} communicates with the other members of the pack.`;
32 }
33};
34
35// Mixin for territorial behavior
36const TerritorialMixin = {
37 markTerritory() {
38 return `${this.name} marks its territory.`;
39 },
40
41 defendTerritory(intruder) {
42 return `${this.name} defends its territory against ${intruder}!`;
43 }
44};
45
46// Function that applies mixins to a class
47function applyMixins(targetClass, ...mixins) {
48 mixins.forEach(mixin => {
49 Object.getOwnPropertyNames(mixin).forEach(name => {
50 if (name !== 'constructor') {
51 targetClass.prototype[name] = mixin[name];
52 }
53 });
54 });
55}
56
57// Velociraptor class with several mixins
58class Velociraptor extends Dinosaur {
59 constructor(name) {
60 super(name, "Velociraptor");
61 this.diet = "carnivore";
62 this.speed = 70; // km/h
63 }
64}
65
66// We apply the mixins to Velociraptor
67applyMixins(Velociraptor, CarnivoreMixin, PackHunterMixin, TerritorialMixin);
68
69// We create a Velociraptor instance
70const blue = new Velociraptor("Blue");
71
72// Now blue has access to methods from different mixins
73console.log(blue.roar()); // "Blue roars!" (from Dinosaur)
74console.log(blue.hunt("Gallimimus")); // "Blue hunts Gallimimus!" (from CarnivoreMixin)
75console.log(blue.bite()); // "Blue bites with great force!" (from CarnivoreMixin)
76console.log(blue.coordinateAttack()); // "Blue coordinates the pack's attack!" (from PackHunterMixin)
77console.log(blue.defendTerritory("Tyrannosaurus")); // "Blue defends its territory against Tyrannosaurus!" (from TerritorialMixin)Mixins let you "mix in" functionality from different sources without having to use multiple inheritance, which JavaScript does not formally support.
Object.prototype
All objects in JavaScript ultimately inherit from Object.prototype. It contains many useful methods, such as toString(), hasOwnProperty(), valueOf(), etc.
1const dino = { name: "Rex" };
2
3// dino inherits methods from Object.prototype
4console.log(dino.toString()); // "[object Object]"
5console.log(dino.hasOwnProperty("name")); // true
6console.log(dino.hasOwnProperty("age")); // false
7
8// We can override methods from Object.prototype
9dino.toString = function() {
10 return `Dinosaur: ${this.name}`;
11};
12
13console.log(dino.toString()); // "Dinosaur: Rex"Prototypal Inheritance in Practice: The Jurassic Park Dinosaur System
Below is an example of a more complex dinosaur management system that uses prototypal inheritance:
1// Base class for all creatures in the park
2class Creature {
3 constructor(name, age) {
4 this.name = name;
5 this.age = age;
6 this.health = 100;
7 this.alive = true;
8 }
9
10 eat(food) {
11 this.health = Math.min(100, this.health + 10);
12 return `${this.name} eats ${food}. Health: ${this.health}%`;
13 }
14
15 sleep() {
16 this.health = Math.min(100, this.health + 5);
17 return `${this.name} is sleeping. Health: ${this.health}%`;
18 }
19
20 damage(amount) {
21 this.health = Math.max(0, this.health - amount);
22 if (this.health === 0 && this.alive) {
23 this.alive = false;
24 return `${this.name} is dead!`;
25 }
26 return `${this.name} takes ${amount} damage. Remaining health: ${this.health}%`;
27 }
28}
29
30// Class for dinosaurs, inheriting from Creature
31class Dinosaur extends Creature {
32 constructor(name, species, age, diet) {
33 super(name, age);
34 this.species = species;
35 this.diet = diet;
36 this.containmentLevel = this.calculateContainmentLevel();
37 }
38
39 calculateContainmentLevel() {
40 if (this.diet === "carnivore") {
41 return this.age > 5 ? "Maximum" : "High";
42 } else {
43 return this.age > 10 ? "Medium" : "Standard";
44 }
45 }
46
47 roar() {
48 const volume = this.diet === "carnivore" ? "loudly" : "quietly";
49 return `${this.name} roars ${volume}!`;
50 }
51
52 getStatus() {
53 return `
54Dinosaur: ${this.name} (${this.species})
55Age: ${this.age} years
56Diet: ${this.diet === "carnivore" ? "Carnivorous" : "Herbivorous"}
57Health: ${this.health}%
58Status: ${this.alive ? "Alive" : "Dead"}
59Security level: ${this.containmentLevel}
60`;
61 }
62}
63
64// Class for carnivorous dinosaurs (predators)
65class Carnivore extends Dinosaur {
66 constructor(name, species, age, attackPower) {
67 super(name, species, age, "carnivore");
68 this.attackPower = attackPower;
69 this.huntingSuccess = 0.7; // 70% chance of a successful hunt
70 }
71
72 hunt(prey) {
73 if (!this.alive) return `${this.name} is dead and cannot hunt.`;
74 if (!prey.alive) return `${prey.name} is already dead.`;
75
76 const huntSuccess = Math.random() < this.huntingSuccess;
77
78 if (huntSuccess) {
79 const damage = Math.floor(this.attackPower * (0.7 + Math.random() * 0.6)); // 70-130% of attack power
80 const result = prey.damage(damage);
81 this.eat("meat");
82 return `${this.name} successfully hunts ${prey.name}! ${result}`;
83 } else {
84 return `${this.name} tries to hunt ${prey.name}, but fails.`;
85 }
86 }
87
88 clawAttack(target) {
89 if (!this.alive) return `${this.name} is dead and cannot attack.`;
90 if (!target.alive) return `${target.name} is already dead.`;
91
92 const damage = Math.floor(this.attackPower * 0.6);
93 const result = target.damage(damage);
94 return `${this.name} attacks ${target.name} with its claws! ${result}`;
95 }
96
97 biteAttack(target) {
98 if (!this.alive) return `${this.name} is dead and cannot attack.`;
99 if (!target.alive) return `${target.name} is already dead.`;
100
101 const damage = Math.floor(this.attackPower * 1.2);
102 const result = target.damage(damage);
103 return `${this.name} bites ${target.name}! ${result}`;
104 }
105}
106
107// Class for herbivorous dinosaurs
108class Herbivore extends Dinosaur {
109 constructor(name, species, age, defenseRating) {
110 super(name, species, age, "herbivore");
111 this.defenseRating = defenseRating;
112 this.fleeSuccess = 0.6; // 60% chance of a successful escape
113 }
114
115 grazeVegetation() {
116 if (!this.alive) return `${this.name} is dead and cannot eat.`;
117
118 this.health = Math.min(100, this.health + 15);
119 return `${this.name} nibbles on plants. Health rises to ${this.health}%.`;
120 }
121
122 flee(predator) {
123 if (!this.alive) return `${this.name} is dead and cannot flee.`;
124
125 const escapeSuccess = Math.random() < this.fleeSuccess;
126
127 if (escapeSuccess) {
128 this.health = Math.max(0, this.health - 5); // Fleeing costs energy
129 return `${this.name} successfully escapes from ${predator.name}!`;
130 } else {
131 return `${this.name} tries to escape from ${predator.name}, but fails.`;
132 }
133 }
134
135 // Defensive method - reduces the damage taken
136 damage(amount) {
137 // Herbivores take less damage thanks to defenseRating
138 const reducedDamage = Math.max(0, amount - this.defenseRating);
139 return super.damage(reducedDamage);
140 }
141}
142
143// Specific dinosaur species
144class Tyrannosaurus extends Carnivore {
145 constructor(name, age) {
146 super(name, "Tyrannosaurus Rex", age, 35);
147 this.intimidationFactor = 9;
148 this.territorialRange = 5; // km
149 }
150
151 intimidate(target) {
152 return `${this.name} lets out a deafening roar that terrifies ${target.name}!`;
153 }
154
155 // We override the roar method
156 roar() {
157 return `${this.name} lets out a mighty, deafening roar that carries for kilometers!`;
158 }
159}
160
161class Velociraptor extends Carnivore {
162 constructor(name, age) {
163 super(name, "Velociraptor", age, 20);
164 this.speed = 70; // km/h
165 this.intelligence = 9;
166 this.packHunting = true;
167 }
168
169 coordinateAttack(targets) {
170 if (!Array.isArray(targets) || targets.length < 2) {
171 return `${this.name} needs at least two targets to coordinate an attack.`;
172 }
173
174 let results = `${this.name} coordinates a pack attack on ${targets.map(t => t.name).join(', ')}!\n`;
175 targets.forEach(target => {
176 const damage = Math.floor(this.attackPower * 1.5); // Boosted pack attack
177 results += target.damage(damage) + '\n';
178 });
179
180 return results;
181 }
182
183 ambush(target) {
184 const damage = Math.floor(this.attackPower * 2); // Double damage from an ambush
185 const result = target.damage(damage);
186 return `${this.name} ambushes ${target.name}! ${result}`;
187 }
188}
189
190class Triceratops extends Herbivore {
191 constructor(name, age) {
192 super(name, "Triceratops", age, 25);
193 this.hornStrength = 30;
194 }
195
196 chargeAttack(target) {
197 const damage = this.hornStrength + Math.floor(Math.random() * 10);
198 const result = target.damage(damage);
199 return `${this.name} charges at ${target.name} with its horns! ${result}`;
200 }
201
202 defendFromAttack(attacker) {
203 const counterDamage = Math.floor(this.hornStrength * 0.5);
204 const result = attacker.damage(counterDamage);
205 return `${this.name} defends itself against ${attacker.name} using its horns! ${result}`;
206 }
207}
208
209class Brachiosaurus extends Herbivore {
210 constructor(name, age) {
211 super(name, "Brachiosaurus", age, 15);
212 this.height = 13; // meters
213 this.tailLength = 9; // meters
214 }
215
216 reachHighVegetation() {
217 this.health = Math.min(100, this.health + 20);
218 return `${this.name} stretches its long neck to reach the leaves of tall trees. Health rises to ${this.health}%.`;
219 }
220
221 tailWhip(target) {
222 const damage = Math.floor(this.tailLength * 2);
223 const result = target.damage(damage);
224 return `${this.name} strikes ${target.name} with its enormous tail! ${result}`;
225 }
226}
227
228// Jurassic Park simulation
229function runJurassicParkSimulation() {
230 console.log("=== JURASSIC PARK SIMULATION ===\n");
231
232 // Creating the dinosaurs
233 const rex = new Tyrannosaurus("Rex", 8);
234 const blue = new Velociraptor("Blue", 4);
235 const delta = new Velociraptor("Delta", 4);
236 const trixie = new Triceratops("Trixie", 12);
237 const bronty = new Brachiosaurus("Bronty", 25);
238
239 // Displaying information about the dinosaurs
240 console.log(rex.getStatus());
241 console.log(blue.getStatus());
242 console.log(trixie.getStatus());
243 console.log(bronty.getStatus());
244
245 console.log("\n=== DINOSAUR INTERACTIONS ===\n");
246
247 // Interactions between dinosaurs
248 console.log(rex.roar());
249 console.log(blue.roar());
250 console.log(trixie.grazeVegetation());
251 console.log(bronty.reachHighVegetation());
252
253 console.log("\n=== DUEL: REX VS TRIXIE ===\n");
254
255 console.log(rex.intimidate(trixie));
256 console.log(trixie.flee(rex));
257 console.log(rex.hunt(trixie));
258 console.log(trixie.defendFromAttack(rex));
259 console.log(rex.biteAttack(trixie));
260
261 console.log("\n=== VELOCIRAPTOR PACK HUNT ===\n");
262
263 console.log(blue.coordinateAttack([trixie, bronty]));
264 console.log(delta.ambush(bronty));
265 console.log(bronty.tailWhip(delta));
266
267 console.log("\n=== DINOSAUR STATUS AFTER THE INTERACTIONS ===\n");
268
269 console.log(rex.getStatus());
270 console.log(blue.getStatus());
271 console.log(delta.getStatus());
272 console.log(trixie.getStatus());
273 console.log(bronty.getStatus());
274}
275
276// Running the simulation
277runJurassicParkSimulation();Prototypal Inheritance vs Classical Inheritance
Prototypal inheritance in JavaScript differs from classical inheritance in other object-oriented languages:
- Dynamism - prototypes can be modified while the program is running, whereas classes are usually static.
- The prototype chain - properties are looked up along the prototype chain, whereas in classical inheritance methods are resolved through the class hierarchy.
- No classes (before ES6) - JavaScript traditionally had no formal concept of classes, only constructor functions and prototypes.
- Single inheritance - JavaScript supports only single inheritance (each object has just one prototype), whereas some languages support multiple inheritance.
Best Practices When Working with Prototypes
- Understand the mechanism - it is important to understand how the prototype chain works in order to use inheritance effectively.
- Use classes (ES6+) - for most use cases the class syntax is more readable and easier to maintain.
- Avoid modifying built-in prototypes - modifying the prototypes of built-in objects (e.g. Object.prototype, Array.prototype) can lead to unexpected conflicts.
- Use Object.create() for inheritance - when you take the traditional approach, Object.create() is a clean way to implement prototypal inheritance.
- Composition over inheritance - sometimes it is better to use composition (combining objects) instead of deep inheritance, to avoid problems with the hierarchy.
Summary
Prototypes and prototypal inheritance are fundamental concepts in JavaScript. Even when we use the modern class syntax (ES6+), it is important to understand the underlying prototype mechanism in order to make full use of the language. In our Jurassic Park, prototypal inheritance helps us model different types of dinosaurs and their behaviors in a consistent, efficient way.
The main concepts we covered are:
- Prototypes - the mechanism that lets objects inherit properties and methods from other objects.
- The prototype chain - a sequence of objects linked through their prototypes, which JavaScript searches when it tries to access a property.
- Constructor functions and prototypes - the traditional way of implementing inheritance in JavaScript.
- ES6+ classes - a modern syntax for inheritance, built on the prototype mechanism.
- Mixins - a way to combine functionality from different sources.
Understanding prototypes and inheritance in JavaScript is the key to writing efficient, well-organized code that can model complex relationships between objects, such as those in our virtual Jurassic Park.
Code for this lesson: index.js
1// Prototypes and inheritance - Jurassic Park
2console.log("Prototypes and Inheritance");
3console.log("Advanced OOP concepts in JavaScript\n");
4
5// ===========================================
6// 1. PROTOTYPE BASICS
7// ===========================================
8console.log("=== 1. What are prototypes? ===");
9
10function Dinosaur(name, species) {
11 this.name = name;
12 this.species = species;
13}
14
15// Adding methods to the prototype
16Dinosaur.prototype.roar = function() {
17 return `${this.name} says: ROAARRR!`;
18};
19
20Dinosaur.prototype.getInfo = function() {
21 return `${this.name} (${this.species})`;
22};
23
24const trex = new Dinosaur("Rexy", "T-Rex");
25const raptor = new Dinosaur("Blue", "Velociraptor");
26
27console.log("\n", trex.getInfo());
28console.log(" ", trex.roar());
29console.log("\n", raptor.getInfo());
30console.log(" ", raptor.roar());
31
32// Checking the prototype
33console.log("\nChecking the prototype:");
34console.log(` trex.__proto__ === Dinosaur.prototype: ${trex.__proto__ === Dinosaur.prototype}`);
35console.log(` raptor.constructor === Dinosaur: ${raptor.constructor === Dinosaur}`);
36
37// ===========================================
38// 2. PROTOTYPE CHAIN
39// ===========================================
40console.log("\n=== 2. Prototype chain ===");
41
42console.log("\nPrototype chain for trex:");
43console.log(` trex β Dinosaur.prototype β Object.prototype β null`);
44
45console.log("\nMethods accessible via the chain:");
46console.log(` trex.getInfo() (from Dinosaur.prototype): ${trex.getInfo()}`);
47console.log(` trex.toString() (from Object.prototype): ${trex.toString()}`);
48console.log(` trex.hasOwnProperty('name'): ${trex.hasOwnProperty('name')}`);
49
50// Checking in the chain
51console.log("\ninstanceof:");
52console.log(` trex instanceof Dinosaur: ${trex instanceof Dinosaur}`);
53console.log(` trex instanceof Object: ${trex instanceof Object}`);
54
55// ===========================================
56// 3. PROTOTYPAL INHERITANCE
57// ===========================================
58console.log("\n=== 3. Prototypal inheritance ===");
59
60function Animal(name, age) {
61 this.name = name;
62 this.age = age;
63}
64
65Animal.prototype.breathe = function() {
66 return `${this.name} breathes`;
67};
68
69Animal.prototype.getAge = function() {
70 return `${this.name} is ${this.age} years old`;
71};
72
73// Carnivore inherits from Animal
74function Carnivore(name, age, huntingSkill) {
75 Animal.call(this, name, age); // Call the parent constructor
76 this.huntingSkill = huntingSkill;
77}
78
79// Setting up inheritance
80Carnivore.prototype = Object.create(Animal.prototype);
81Carnivore.prototype.constructor = Carnivore;
82
83// Adding own methods
84Carnivore.prototype.hunt = function() {
85 return `${this.name} hunts (skill: ${this.huntingSkill}/10)`;
86};
87
88Carnivore.prototype.roar = function() {
89 return `${this.name} roars: ROAARRR!`;
90};
91
92const rex = new Carnivore("Rex", 8, 10);
93
94console.log("\nCarnivore (inherits from Animal):");
95console.log(` ${rex.getAge()}`); // From Animal
96console.log(` ${rex.breathe()}`); // From Animal
97console.log(` ${rex.hunt()}`); // Own method
98console.log(` ${rex.roar()}`); // Own method
99
100// ===========================================
101// 4. ES6 CLASSES (SYNTACTIC SUGAR)
102// ===========================================
103console.log("\n=== 4. ES6 Classes ===");
104
105class DinoBase {
106 constructor(name, species, age) {
107 this.name = name;
108 this.species = species;
109 this.age = age;
110 }
111
112 getInfo() {
113 return `${this.name} (${this.species}) - ${this.age} years`;
114 }
115
116 breathe() {
117 return `${this.name} breathes`;
118 }
119}
120
121class CarnivoreClass extends DinoBase {
122 constructor(name, species, age, huntingSkill) {
123 super(name, species, age);
124 this.huntingSkill = huntingSkill;
125 this.type = "Carnivore";
126 }
127
128 hunt() {
129 return `${this.name} hunts (skill: ${this.huntingSkill}/10)`;
130 }
131
132 roar() {
133 return `${this.name} roars: ROAARRR!`;
134 }
135
136 // Method override from the base class
137 getInfo() {
138 return `${super.getInfo()} | Type: ${this.type}`;
139 }
140}
141
142class HerbivoreClass extends DinoBase {
143 constructor(name, species, age, plantPreference) {
144 super(name, species, age);
145 this.plantPreference = plantPreference;
146 this.type = "Herbivore";
147 }
148
149 eat() {
150 return `${this.name} eats ${this.plantPreference}`;
151 }
152
153 graze() {
154 return `${this.name} peacefully grazes`;
155 }
156
157 getInfo() {
158 return `${super.getInfo()} | Type: ${this.type}`;
159 }
160}
161
162const rexy = new CarnivoreClass("Rexy", "T-Rex", 8, 10);
163const trike = new HerbivoreClass("Trike", "Triceratops", 7, "ferns");
164
165console.log("\nCarnivore:");
166console.log(` ${rexy.getInfo()}`);
167console.log(` ${rexy.breathe()}`);
168console.log(` ${rexy.hunt()}`);
169console.log(` ${rexy.roar()}`);
170
171console.log("\nHerbivore:");
172console.log(` ${trike.getInfo()}`);
173console.log(` ${trike.breathe()}`);
174console.log(` ${trike.eat()}`);
175console.log(` ${trike.graze()}`);
176
177// ===========================================
178// 5. MULTI-LEVEL INHERITANCE
179// ===========================================
180console.log("\n=== 5. Multi-level inheritance ===");
181
182class LivingBeing {
183 constructor(name) {
184 this.name = name;
185 }
186
187 live() {
188 return `${this.name} is alive`;
189 }
190}
191
192class Reptile extends LivingBeing {
193 constructor(name, scaleType) {
194 super(name);
195 this.scaleType = scaleType;
196 }
197
198 shedSkin() {
199 return `${this.name} sheds its skin`;
200 }
201}
202
203class PrehistoricReptile extends Reptile {
204 constructor(name, scaleType, period) {
205 super(name, scaleType);
206 this.period = period;
207 this.extinct = true;
208 }
209
210 getHistory() {
211 return `${this.name} lived in the ${this.period} period`;
212 }
213}
214
215const velociraptor = new PrehistoricReptile("Blue", "small", "Cretaceous");
216
217console.log("\nVelociraptor (3 levels of inheritance):");
218console.log(` ${velociraptor.live()}`); // From LivingBeing
219console.log(` ${velociraptor.shedSkin()}`); // From Reptile
220console.log(` ${velociraptor.getHistory()}`); // From PrehistoricReptile
221
222console.log("\ninstanceof (multi-level):");
223console.log(` instanceof PrehistoricReptile: ${velociraptor instanceof PrehistoricReptile}`);
224console.log(` instanceof Reptile: ${velociraptor instanceof Reptile}`);
225console.log(` instanceof LivingBeing: ${velociraptor instanceof LivingBeing}`);
226console.log(` instanceof Object: ${velociraptor instanceof Object}`);
227
228// ===========================================
229// 6. MIXINS (COMPOSITION OVER INHERITANCE)
230// ===========================================
231console.log("\n=== 6. Mixins - composition instead of inheritance ===");
232
233// Mixin for flying capabilities
234const FlyingMixin = {
235 fly() {
236 return `${this.name} flies at a speed of ${this.flyingSpeed} km/h`;
237 },
238 land() {
239 return `${this.name} lands`;
240 }
241};
242
243// Mixin for swimming capabilities
244const SwimmingMixin = {
245 swim() {
246 return `${this.name} swims`;
247 },
248 dive() {
249 return `${this.name} dives`;
250 }
251};
252
253// Helper function for adding mixins
254function addMixins(targetClass, ...mixins) {
255 mixins.forEach(mixin => {
256 Object.assign(targetClass.prototype, mixin);
257 });
258}
259
260class Pteranodon {
261 constructor(name, flyingSpeed) {
262 this.name = name;
263 this.flyingSpeed = flyingSpeed;
264 }
265
266 roar() {
267 return `${this.name} screeches: SCREECH!`;
268 }
269}
270
271class Mosasaurus {
272 constructor(name) {
273 this.name = name;
274 }
275
276 roar() {
277 return `${this.name} roars underwater: ROAARRR!`;
278 }
279}
280
281// Adding mixins
282addMixins(Pteranodon, FlyingMixin);
283addMixins(Mosasaurus, SwimmingMixin);
284
285const ptero = new Pteranodon("Pterry", 80);
286const mosa = new Mosasaurus("Mosa");
287
288console.log("\nPteranodon (with the flying mixin):");
289console.log(` ${ptero.roar()}`);
290console.log(` ${ptero.fly()}`);
291console.log(` ${ptero.land()}`);
292
293console.log("\nMosasaurus (with the swimming mixin):");
294console.log(` ${mosa.roar()}`);
295console.log(` ${mosa.swim()}`);
296console.log(` ${mosa.dive()}`);
297
298// ===========================================
299// 7. COMPLEX EXAMPLE
300// ===========================================
301console.log("\n=== 7. Ecosystem simulation - complex example ===");
302
303class Ecosystem {
304 constructor(name) {
305 this.name = name;
306 this.creatures = [];
307 }
308
309 addCreature(creature) {
310 this.creatures.push(creature);
311 console.log(`Added ${creature.name} to ecosystem ${this.name}`);
312 }
313
314 simulate() {
315 console.log(`\n============================================================`);
316 console.log(`ECOSYSTEM SIMULATION: ${this.name}`);
317 console.log("=".repeat(60));
318
319 console.log(`\nTotal creatures: ${this.creatures.length}`);
320
321 const carnivores = this.creatures.filter(c => c.type === "Carnivore");
322 const herbivores = this.creatures.filter(c => c.type === "Herbivore");
323
324 console.log(` Carnivores: ${carnivores.length}`);
325 console.log(` Herbivores: ${herbivores.length}`);
326
327 console.log("\nCreature activities:");
328 this.creatures.forEach((creature, i) => {
329 console.log(`\n ${i + 1}. ${creature.getInfo()}`);
330 console.log(` ${creature.breathe()}`);
331
332 if (creature.hunt) {
333 console.log(` ${creature.hunt()}`);
334 console.log(` ${creature.roar()}`);
335 }
336
337 if (creature.eat) {
338 console.log(` ${creature.eat()}`);
339 console.log(` ${creature.graze()}`);
340 }
341 });
342
343 this.showFoodChain();
344 }
345
346 showFoodChain() {
347 console.log("\nFood chain:");
348
349 const carnivores = this.creatures.filter(c => c.type === "Carnivore");
350 const herbivores = this.creatures.filter(c => c.type === "Herbivore");
351
352 console.log(` Plants β Herbivores (${herbivores.map(h => h.name).join(", ")})`);
353 console.log(` Herbivores β Carnivores (${carnivores.map(c => c.name).join(", ")})`);
354 }
355}
356
357// Creating the ecosystem
358const jurassicEcosystem = new Ecosystem("Jurassic Park");
359
360// Adding creatures
361jurassicEcosystem.addCreature(new CarnivoreClass("Rexy", "T-Rex", 8, 10));
362jurassicEcosystem.addCreature(new CarnivoreClass("Blue", "Velociraptor", 5, 9));
363jurassicEcosystem.addCreature(new HerbivoreClass("Trike", "Triceratops", 7, "ferns"));
364jurassicEcosystem.addCreature(new HerbivoreClass("Stego", "Stegosaurus", 6, "plants"));
365jurassicEcosystem.addCreature(new CarnivoreClass("Delta", "Velociraptor", 5, 8));
366
367// Run the simulation
368jurassicEcosystem.simulate();
369
370console.log("\nCongratulations! You've mastered prototypes and inheritance!");Spotted a mistake in this lesson?
Check yourself
Answer the questions from this lesson. Pick an answer to see right away whether it is correct.
1. What happens when you try to access a property that the object doesn't have?
2. What does the Object.create(proto) method do?
Hands-on tasks in the game
- Vertical ordering
Arrange the steps of searching for a property in the prototype chain: