Javascript 子扩展类方法调用其超级版本,但仍然只看到子数据

Javascript 子扩展类方法调用其超级版本,但仍然只看到子数据,javascript,inheritance,ecmascript-6,constructor,Javascript,Inheritance,Ecmascript 6,Constructor,B类扩展了A类。我将A称为父类,B称为子类。两者都有构造函数。B在其构造函数内调用super()。两者都有一个同名的方法。也许只是巧合或错误,两者都有一个'this.x'变量。这样就无法访问父级的this.x变量。然后,它就成为孩子和父母之间可能是无意中交流的一个点 class A { constructor(){ this.x = "super x!"; } logx(){ console.log(this.x);

B类扩展了A类。我将A称为父类,B称为子类。两者都有构造函数。B在其构造函数内调用super()。两者都有一个同名的方法。也许只是巧合或错误,两者都有一个'this.x'变量。这样就无法访问父级的this.x变量。然后,它就成为孩子和父母之间可能是无意中交流的一个点

   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".
可能是类A来自库,或者是由其他人编写的。甚至可能出现这样的情况:类A的作者来编辑代码,并添加一个新的变量,然后该变量对一个他甚至不知道存在的孩子使用别名。然后,子类的作者必须成为父类更改的热心读者,这样他或她就可以相应地更新他或她自己的代码,如果这个作者确实还在项目中的话。(正是这样一个bug让我今天来到这里,这是它的精华。)

在下面的代码中,我通过给每个变量一个与类名相同的前缀来防止这个问题。然后我得到了预期的行为。当然还有更好的办法。也许这些私有/公共关键字中的一些会有所帮助

      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!"
方法调用也会发生这种情况,尽管这并不令人惊讶,因为a)被认为是“多态性”b)上游代码接口的更改通常会对下游代码产生影响。但是,程序员可能有一些辅助函数不在接口上,如果子类作者碰巧想到了相同的辅助函数名,或者使用该名称的函数扩展接口

   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();
控制台输出:

I am derived f()
derived x.
    I am a super f()!
    super x!
根据Jonas Wilms对其正在发生的事情的解释,确实可以使用合成模式来封装父级数据,从而防止意外出现别名:

   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();
它的行为符合预期,控制台输出:

I am derived f()
derived x.
    I am a super f()!
    super x!
然而,这并非没有问题。首先,instanceof操作符不起作用。其次,我们不继承任何方法。子类的作者必须添加只接受参数的存根,并将它们传递给父类方法。这可能会影响性能。看


。。这个问题似乎可以归结为“如何定义接口上的内容,以及接口上的内容是什么?”天啊,这里有一个演示,说明了为什么有人会喜欢这样做。

实际上,您的
层次结构等于

 // 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);
因此,虽然实际上有两个
logx
方法可供参考(一个在A的原型上,一个在B的原型上),但只有一个实例(
this
)在构建过程中通过,并且设置对象的属性会覆盖以前的值。因此,您是对的,没有办法用相同的名称拥有不同的属性

天哪,如果你想确保父变量保持独立,你不需要做一些事情,比如采用一种惯例,根据每个变量的类名给它一个前缀

我真的建议使用Typescript来关注结构(有一个
private
readonly
属性修饰符)。在JS中,您可以使用符号来模拟私有属性:1

 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]);
(当然,您可以将符号保留在任何类型的变量中,无需将其作为类的一部分)

或者你放弃继承的东西,用A组成B:

 class B {
   constructor() {
     this.a = new A();
     this.x = "new x";
   }
 }

 (new B).x
 (new B).a.x
方法调用也会发生这种情况吗

是,因为B实例的继承链是:

 b -> B.prototype -> A.prototype
该方法将首先在
b
中查找,然后在
b
中查找,最后在
A
中查找,因此如果在A和b中都有一个名为“logx”的方法,则将取b中的一个。你也可以这样做:

 b.logx = function() { console.log("I'm first!");
那么,如果想要父f(),在编写父代码时该怎么做

您可以直接在原型上调用它:

 A.prototype.logx.call(b /*...arguments*/);
从一个方法中,您可以采用
this
,而不是具体的实例(本例中为
b
)。如果您不想采用特定的实现,而是采用超类的实现,请像以前一样使用
super.logx()



老实说:我从来没有遇到过任何问题,只要你正确命名属性,名称就很少冲突。

很好的解释-如果你来自其他OO语言,这是JS中非常令人困惑的一部分。所以我想如果我从一个超类中调用一个函数,比如说这个.f(),也会发生同样的情况,然后它将调用在子类中定义的这个.f()。。。我去试试这个..@user244488是的,没错。但这实际上就是多态性的含义,CLOS也会做类似的事情。但是,C++中的多态性通过虚拟指针表,我不相信父调用静态编译的父方法,也就是说,不通过指针调用,将转到子方法。在CLOS中,由于parents函数将all循环调用回子对象,因此很难让父对象作为子对象的助手来处理父对象的内容。举个简单的例子,如果我有一个二维向量,由父向量负责,而子向量添加了另一个维度,并在前两个维度中使用父向量。@mark实际上,我认为这一点越来越糟糕,因为委员会正专注于扩展类语法(私有字段、字段初始值设定项等)。这让事情变得更加混乱(即使对我来说,我也是在ES5中长大的)。(哦,谢谢:))基于到目前为止的互动,我只是更新了描述,希望能更清楚一点。我删除了当前的解决方案检查(对不起,Jonas)。虽然现在对这个问题有了更好的理解,但仍然没有给出解决方案。现在这个问题已经被更好地理解了,也许解决方案对某些人来说会更明显。我认为没有解决方案(除了打字脚本)。那是朱