JavaScript and TypeScript course Β· Module 2: Functions, Arrays and Objects

Prototypes and Prototypal Inheritance

16 min read
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')); // true

In 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); // true

In 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); // true

This 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); // true

Prototypes 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); // true

The 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:

  1. Dynamism - prototypes can be modified while the program is running, whereas classes are usually static.
  2. The prototype chain - properties are looked up along the prototype chain, whereas in classical inheritance methods are resolved through the class hierarchy.
  3. No classes (before ES6) - JavaScript traditionally had no formal concept of classes, only constructor functions and prototypes.
  4. 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

  1. Understand the mechanism - it is important to understand how the prototype chain works in order to use inheritance effectively.
  2. Use classes (ES6+) - for most use cases the class syntax is more readable and easier to maintain.
  3. Avoid modifying built-in prototypes - modifying the prototypes of built-in objects (e.g. Object.prototype, Array.prototype) can lead to unexpected conflicts.
  4. Use Object.create() for inheritance - when you take the traditional approach, Object.create() is a clean way to implement prototypal inheritance.
  5. 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:

  1. Prototypes - the mechanism that lets objects inherit properties and methods from other objects.
  2. The prototype chain - a sequence of objects linked through their prototypes, which JavaScript searches when it tries to access a property.
  3. Constructor functions and prototypes - the traditional way of implementing inheritance in JavaScript.
  4. ES6+ classes - a modern syntax for inheritance, built on the prototype mechanism.
  5. 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. 1. What happens when you try to access a property that the object doesn't have?

  2. 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:

Useful articles