class B extends class A. I'll call A the parent and B the child. Both have constructors. B calls super() inside of its constructor. Both have a method with the same name. Perhaps just by coincidence or mistake both have a 'this.x' variable. There then becomes no way to access the parent's this.x variable. It then becomes a point of perhaps unintended communication between the child and parent.
class A {
constructor(){
this.x = "super x!";
}
logx(){
console.log(this.x);
}
}
class B extends A{
constructor(){
super();
this.x = "derived x.";
}
logx(){
super.logx();
}
}
let b = new B;
b.logx(); // expected "super x!", but it prints "derived x".
It might be the case that class A comes from a library, or was written by someone else. It might even be the case that the author of class A comes and edits the code and adds a new variable, which then aliases against a child that he doesn't even know exists. The author of the child class must then become an avid reader of changes in the parent class so he or she can update his or her own code accordingly, if indeed this author is even still on the project. (It is such a bug that has lead me here today, this is the distillation of it.)
In the following code I prevent this problem by giving every variable a prefix that is the same as the class name. Then I get the expected behavior. Surely there is a better way. Perhaps some of these private / public keywords would help?
constructor(){
this.A_x = "super x!";
}
logx(){
console.log(this.A_x);
}
}
class B extends A{
constructor(){
super();
this.B_x = "derived x.";
}
logx(){
super.logx();
}
}
let b = new B;
b.logx(); // expected "super x!", and indeed it prints "super x!"
This also happens for method calls, though that is less surprising because a) that is considered 'polymorphism' b) it is usual that changes to the interface of upstream code has downstream code effects. However, a programmer might have some auxiliary functions not intended to be on the interface, and if a child class author happens to think of the same auxiliary function name, or extends the interface with a function by that name ...
class A {
constructor(){
this.x = "super x!";
}
f(){
console.log("I am a super f()!");
}
logx(){
this.f(); // aliased - polymorphism behavior
console.log(this.x);
}
}
class B extends A{
constructor(){
super();
this.x = "derived x.";
}
f(){
console.log("I am a derived f()");
}
logx(){
super.logx();
}
}
let b = new B;
b.logx();
console output:
I am derived f()
derived x.
As per Jonas Wilms comment on his unwinding of what is going on, it is true that the composition pattern can be used to encapsulate the parent's data and thus prevent aliasing by accident:
class A {
constructor(){
this.x = "super x!";
}
f(){
console.log("I am a super f()!");
}
logx(){
this.f();
console.log(this.x);
}
}
class B {
constructor(){
this.a = new A();
this.x = "derived x.";
}
f(){
console.log("I am a derived f()");
}
logx(){
this.a.logx();
}
}
let b = new B;
b.logx();
And it behaves as expected, the console output:
I am a super f()!
super x!
However, this is not without its problems. Firstly, the instanceof operator does not work. Secondly, we don't inherit any methods. The author of the child class will have to add stubs that just take the arguments and pass them to the parent class method. This might have performance implications. See ES6 et al. is it possible to define a catch-all method?.
.. it seems this question boils down to, 'how do you define what is on the interface, and what isn't?' and gee, there is a demonstration of why someone might like to do this.