javascript - 子扩展类方法调用其超级版本,但仍然只能看到子数据
问题描述
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 等。是否可以定义一个包罗万象的方法?.
.. 似乎这个问题归结为,“你如何定义界面上的内容,以及不存在的内容?” 哎呀,有一个演示为什么有人可能喜欢这样做。
解决方案
实际上你的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 来关注结构(有一个private
andreadonly
属性修饰符)。在 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()
。
¹说实话:我从来没有遇到过任何问题,只要您正确命名您的属性,名称几乎不会发生冲突。
推荐阅读
- ruby-on-rails - Custom devise method : render 'new' redirect to /users instead of /users/sign_up
- r - How to perform a bootstrap and find 95% confidence interval for the median of a dataset
- java - Get annotations when exec-maven-plugin runs Main does not work
- docker - jhipster 微服务在 docker 和本地混合运行。网关无法访问 UAA
- node.js - Socrata nodejs - 将浮动时间戳转换为日期时间格式
- javascript - Puppeteer - 使用“--allow-file-access-from-files”通过 XMLHttpRequest 加载本地文件不起作用
- scip - SCIP 6.0 无法读取的 SMPS 文件
- alias - 带有选项的别名的 kubectl bash 补全
- javascript - pug 迭代 - 循环遍历具有动态长度的对象属性
- user-interface - 在 wxpython 面板上不正确地刷新 GUI 图