What this capstone brings together
The program in this lesson uses every idea from the object-oriented chapter in one small design:
- Inheritance.
Dog,CatandBirdall extend one parent,Animal. - Constructors.
Animalstores thename, and the subclasses reuse that constructor. - An abstract method.
Animaldeclaressound()but gives it no real behaviour. Each child must supply its own. - Shared behaviour.
Animalhas a finishedspeak()that callssound(), so every subclass gets it for free. - Polymorphism. One array holds a dog, a cat and a bird, and one loop treats them all the same way while each prints its own sound.
The shape matters more than the animals: a small contract at the top, concrete variants below, and the rest of the program written against the parent type. A new variant can be added without changing the code that uses the animals.
The starting point
The lesson gives you the parent class:
class Animal {
constructor(name) { this.name = name; }
sound() { throw new Error("Not implemented"); }
speak() {
console.log(this.name + ": " + this.sound());
}
}
JavaScript has no abstract keyword, so sound() is an abstract method by convention: its body throws. A subclass that forgets to override it fails with Not implemented the first time speak() is called on it. speak() is the concrete, shared method. It builds a line from this.name and whatever this.sound() returns.
The task asks for three subclasses and a loop. The output must be, in this order:
Rex: Woof
Whiskers: Meow
Tweety: Tweet
Step by step
Step 1: three subclasses
Write class Dog extends Animal, and the same for Cat and Bird. Each class needs one method, sound(), which returns its string: "Woof", "Meow" and "Tweet". It returns the text rather than printing it, because the inherited speak() concatenates the result into a longer line and does the printing itself. A sound() that logged would print the bare sound and then speak() would build "Rex: undefined".
The subclasses define no constructor. When a class has none, JavaScript supplies one that passes all its arguments to the parent constructor, so new Dog("Rex") runs Animal's constructor and sets this.name. You would write an explicit constructor, calling super(name) first, only if a subclass needed extra fields of its own.
Step 2: the polymorphic collection and loop
Build an array named animals holding new Dog("Rex"), new Cat("Whiskers") and new Bird("Tweety"), in that order, because the output follows the array's order. Then loop with for (let a of animals) and call a.speak().
The loop never asks what kind of animal it has. When it calls a.speak(), JavaScript finds speak on Animal. Inside it, this is the actual object, and this.sound() is looked up starting from that object's own class. So the dog's turn reaches Dog's sound(), the cat's reaches Cat's, and so on. That lookup at run time is what polymorphism means here.
The solution in JavaScript
class Animal {
constructor(name) { this.name = name; }
sound() { throw new Error("Not implemented"); }
speak() {
console.log(this.name + ": " + this.sound());
}
}
class Dog extends Animal {
sound() { return "Woof"; }
}
class Cat extends Animal {
sound() { return "Meow"; }
}
class Bird extends Animal {
sound() { return "Tweet"; }
}
let animals = [new Dog("Rex"), new Cat("Whiskers"), new Bird("Tweety")];
for (let a of animals) {
a.speak();
}
Running it prints the three expected lines. A few JavaScript details are worth noting:
for...ofversusfor...in.for (let a of animals)gives you the elements.for (let a in animals)would give the index keys as strings,"0","1","2", anda.speak()would fail on a string.- Method lookup goes up the prototype chain. A
Dogobject has nospeakof its own. The call finds it onAnimal.prototype, which is whereextendsconnectsDog.prototypeto. - The overridden method wins.
soundexists on bothAnimalandDog, and the lookup stops at the first one it finds, which isDog's. - Classes are not hoisted like functions. Each
classmust appear before the line that creates its instances. The array is built after the three classes for that reason.
Extending the design
To add a cow, write a fourth class with one method and put an instance in the array. The loop and Animal stay as they are:
class Cow extends Animal {
sound() { return "Moo"; }
}
animals.push(new Cow("Bessie"));
The next pass over animals ends with Bessie: Moo. That is the benefit of writing the consumer against the parent type.
Common mistakes
- Printing instead of returning in
sound(). The result is a line withundefinedin it, since a function without areturngives backundefined. - Wrong order in the array. The lines come out in array order, so Rex, Whiskers, Tweety must stay in that order.
- Forgetting
extends. Without it,Dogdoesn't inheritspeakor the constructor, andnew Dog("Rex").speak()fails withspeak is not a function. - Misspelling an override. A method named
sounds()orSound()overrides nothing. The inherited stub runs and throwsNot implemented. - Calling
new Animal("x").speak().Animalis meant only as a parent. Creating one directly works, butspeak()then throws, because nosound()has been provided. - Calling
super.sound()in a child. That reaches the stub and throws. Return a string instead.
Key takeaways
- Put what all variants share in the parent, and let the children supply only what differs.
- A method that throws is JavaScript's convention for an abstract method.
- Code written against the parent type works for every subclass, including ones added later.
- With polymorphism, the method that runs is chosen by the object's actual class when the call happens.