首页 > 解决方案 > 子扩展类方法调用其超级版本,但仍然只能看到子数据

问题描述

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.

根据 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 a super f()!
    super x!

然而,这并非没有问题。首先,instanceof 运算符不起作用。其次,我们不继承任何方法。子类的作者必须添加只接受参数并将它们传递给父类方法的存根。这可能会对性能产生影响。参见ES6 等。是否可以定义一个包罗万象的方法?.

.. 似乎这个问题归结为,“你如何定义界面上的内容,以及不存在的内容?” 哎呀,有一个演示为什么有人可能喜欢这样做。

标签: javascriptinheritanceecmascript-6constructor

解决方案


实际上你的class层次结构等于

 // 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 来关注结构(有一个privateandreadonly属性修饰符)。在 JS 中,您可以使用符号来模仿私有属性:¹

 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

该方法将首先查找 in b,然后查找 in B,最后查找 in A,因此如果在 A 和 B 中都有名称为“logx”的方法,则将采用 B 的那个。你也可以这样做:

 b.logx = function() { console.log("I'm first!");

那么,在编写父代码时,如果想要父 f(),该怎么办?

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

 A.prototype.logx.call(b /*...arguments*/);

从您可以采用的方法中this代替具体实例(b在本例中)。如果您不想采用具体实现,而是采用类之一,请照常使用super.logx()


¹说实话:我从来没有遇到过任何问题,只要您正确命名您的属性,名称几乎不会发生冲突。


推荐阅读