In the example you've posted the method()
method is inherited in the Teacher
and Student
classes, so it not polymorphic.
If you add a method()
to the Teacher and/or Student sub-classes, then you have some classes that can behave slightly differently depending on the type that they are an instance of
for example
class Person {
constructor(name) {
this.name = name
}
method() {
// I've moved the console.log (side effect) for sake of simplicity
return this.name
}
}
// no need to add constructor because is inherited from Person
// nor method, because it inherits method too
class Student extends Person {}
class Teacher extends Person {
// inherits constructor...
// override method decorating it
method() {
return super.method() + ", is a teacher"
}
}
class Employee extends Person {
static count = 0;
// override constructor necessarily decorating it
constructor(name){
super(name)
this.id = ++this.constructor.count;
}
// override method
method(){
return "employee " +this.id
}
}
Employee.count = 0
// fill a list of different types of Person
var people = [
new Student("James"),
new Teacher("Alastair"),
new Employee("Bob"),
]
// print all the instances
people.forEach(x => console.log(x.method()))
You also have to maintain the compatibility of the classes through the hierarchy (same or wider set of arguments, same or more well defined return type), this is known as the Liskov substitution principle.
IMO JS and ES6 are not the best languages for learning traditional OOP principles due to the prototype based inheritance and the weak type system that they have both.
In JS instead is very easy to compose different behavior (methods) when you make the instances, and then use polymorphism without inheritance, an easy way to achieve this could be the following example
class Person {
constructor({name, ...tail}){
if(!name){
throw new TypeError("person.name is missing")
}
Object.assign(this, {name, ...tail})
}
method(){
return this.name
}
greet(){ return "hi"}
}
class Robot {
constructor(...args){
Object.assign(this, ...args)
if(this.constructor.prototype !== Object.getPrototypeOf(this)) {
// throw if __proto__ pollution on the instance
throw new TypeError("override __proto__")
}
}
method(){
return `a robot that says ${
Math.floor(Math.random() * Number.MAX_SAFE_INTEGER).toString("2")
}`
}
}
// the following code might seem unclear but in return
// it could be used with instances of different classes
function introduce(){
return this.greet() +
", I'm " +
this.constructor.prototype.method.apply(this, arguments)
}
// fill a list of different instances of Person
var people = [
new Person({name: "James"}),
new Person({
name: "Alastair",
method: introduce,
}),
new Person({
name: "Bob",
method: introduce,
greet: () => "hola"
}),
new Robot({
method: introduce,
greet: () => "hello"
})
]
// print all the instances
people.forEach(x => console.log(x.method()))
Anyway at the end of the day polymorphism is when you have different classes that shares the aspect but not the behavior so they have a method with the same name, possibly the same arguments and possibly the same return type, with a different implementation, and you can use them interchangeably.
Let's take a gun, a drill and and an electric screwdriver, they all share the aspect trigger
but if you pull the trigger they all behave differently