问题
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.
回答1:
Actually your class hierarchy is equal to
// a constructor is just a function
function A() {
this.x = "super x!";
}
A.prototype.logx = function() { console.log(this.x); };
function B() {
A.call(this); // "this" gets passed, no new instance gets created
this.x = "derived x";
}
B.prototype = Object.create(A.prototype); // extending a class basically lets the prototype of the class inherit the prototype of the superclass
B.prototype.constructor = B;
B.prototype.logx = function() {
A.prototype.logx.call(this); // we can reference A#logx as it exists on the prototype
};
// using "new" basically creates a new object inheriting the prototype, then executes the constructor on it
let b = Object.create(B.prototype);
B.call(b);
So while there are actually two logx methods that you can reference (one on A's prototype and one on B's), there is just one instance (this) that gets passed through during construction, and setting a property of an object overrides the previous value. Therefore you are right, there is no way to have different properties with the same name.
Gosh hope one doesn't need to do something such as adopt a convention of giving every variable a prefix based on its class name if one wants to assure that parent variables remain independent
I really recommend using Typescript to keep an eye on the structure (there is a private and readonly property modifier). In JS you could use Symbols to mimic private properties:¹
class A {
constructor() {
this[A.x] = "stuff";
}
}
A.x = Symbol();
class B extends A {
constructor() {
this[B.x] = "other stuff";
}
}
B.x = Symbol();
console.log(new B()[A.x]);
(for sure you can keep the Symbols in any kind of variable, no need to make it part of the class).
Or you just give up the inheritance thing, and compose B with A:
class B {
constructor() {
this.a = new A();
this.x = "new x";
}
}
(new B).x
(new B).a.x
Will this also happen for method calls?
Yes, as the inheritance chain for an instance of B is:
b -> B.prototype -> A.prototype
The method will be looked up in b first, then in B and finally in A, so if there is a method with the name "logx" in both A and B, the one of B will be taken. You could also do:
b.logx = function() { console.log("I'm first!");
so, what does one do when writing parent code, if one wants the parent f()?
You could directly call it on the prototype:
A.prototype.logx.call(b /*...arguments*/);
from within a method you can take this instead of a concrete instance (b in this example). If you don't want to to take the specific implementation, but the one of the superclass, use super.logx() just as you did.
¹ To be honest: I never had any problems with that, as long as you name your properties properly, names will really rarely clash.
来源:https://stackoverflow.com/questions/55677926/child-extended-class-method-calls-its-super-version-but-that-still-only-sees-chi