Typescript,静态方法继承

17
我正在使用TypeScript,但在类之间的静态继承方面遇到了问题。
有人能否解释一下以下代码的结果:
class Foo {
    protected static bar: string[] = [];

    public static addBar(bar: string) {
        this.bar.push(bar);
    }

    public static logBar() {
        console.log(this.bar);
    }
}

class Son extends Foo {
    protected static bar: string[] = [];
}

class Daughter extends Foo {}

Foo.addBar('Hello');
Son.addBar('World');
Daughter.addBar('Both ?');
Foo.logBar();
Son.logBar();
Daughter.logBar();

当前结果:

[ 'Hello', 'Both ?' ]
[ 'World' ]
[ 'Hello', 'Both ?' ]
但我想要:
[ 'Hello' ]
[ 'World' ]
[ 'Both ?' ]

我是否有一种解决方案,可以避免重新声明静态bar属性?

谢谢!


移除 static - Jonas Wilms
3
移除 static 关键字后,属性将绑定到实例而非类型。OP 希望该属性绑定到类型。 - snarf
2个回答

29
重要的是要理解staticclass的关键是子类的构造函数从超类的构造函数继承。字面上来说,class不仅在由构造函数创建的实例之间设置继承,构造函数本身也处于继承结构中。 FooSonDaughter的原型。这意味着Daughter.barFoo.bar,它是一个继承属性。但是,你给Son自己的bar属性,有自己的数组,所以在Son上查找bar会找到Son自己的bar,而不是Foo上的bar。以下是发生这种情况的更简单的示例:

class Foo { }
class Son extends Foo { }
class Daughter extends Foo { }

Foo.bar = new Map([["a", "ayy"]]);
console.log(Foo.bar.get("a"));          // "ayy"

// `Son` inherits `bar` from `Foo`:
console.log(Son.bar === Foo.bar);       // true, same Map object
console.log(Son.bar.get("a"));          // "ayy"

// So does `Daughter` -- for now
console.log(Daughter.bar === Foo.bar);  // true, same Map object
console.log(Daughter.bar.get("a"));   // "ayy"

// Retroactively giving `Son` its own static `bar`
Son.bar = new Map();
console.log(Son.bar === Foo.bar);       // false, different Map objects
console.log(Son.bar.get("a"));          // undefined

这就是为什么当你查看Foo.barDaughter.bar时会看到["Hello", "Both ?"]:它们都指向同一个数组的相同bar。但是,当你查看Son.bar时,只会看到["World"],因为它指向不同的数组的另一个bar

要将它们分开,你可能想给每个构造函数都分配自己的bar,尽管你也可以像Nitzan Tomer建议的那样使用Map


更详细地介绍一下事物的组织方式。有点像这样:

const Foo = {};
Foo.bar = [];
const Son = Object.create(Foo);
Son.bar = []; // Overriding Foo's bar
const Daughter = Object.create(Foo);
Foo.bar.push("Hello");
Son.bar.push("World");
Daughter.bar.push("Both ?");
console.log(Foo.bar);
console.log(Son.bar);
console.log(Daughter.bar);

如果你是初次接触,这可能会让人感到惊讶。但是在内存中,你的三个类大致如下:

+−−>Function.prototype
  |
Foo−−−−−−−+−+−>|   (函数)  | 
         / /  +−−−−−−−−−−−−+
        | |   | [[原型]] |−−+   +−−−−−−−−−−−+
        | |   | bar           |−−−−−>|  (数组)  |
        | |   | addBar, 等等.  |      +−−−−−−−−−−−+
        | |   +−−−−−−−−−−−−−−−+      | 长度: 2 |
        | |                          | 0: Hello  |
        | |                          | 1: Both ? |
        | |                          +−−−−−−−−−−−+
        | |
        | |
        | |
        | |
        | |
Daughter−−−+ |
         | |
         | |
         | |
         | |
         | |
Son−−−−−−−>|   (函数)  |
         +−−−−−−−−−−−−−−−+
         | [[原型]] |−−−−−+  +−−−−−−−−−−−+
         | bar           |−−−−−−>|  (数组)  |
         +−−−−−−−−−−−−−−−+        +−−−−−−−−−−+
                                 | 长度: 1 |
                                 | 0: World |
                                 +−−−−−−−−−−+

你说“与Foo的链接已经断开”,但我认为技术术语应该是“被遮蔽”,对吧?也就是说,Foo.bar和Son.bar是两个独立的对象,而Foo.bar仍然在Son.bar的原型链中,但子对象遮蔽了父对象,就像内部作用域可以遮蔽外部作用域中的变量一样。(我说“我认为”是因为我不是专家,但这就是我理解的方式,它对我帮助很大。) - Coderer
@Coderer - 我不认为我听过“shadowed”这个术语与对象属性一起使用(只是嵌套作用域中的变量),但从概念上讲,它是类似的。因为Son有自己的bar,在Son上查找bar时,当它在Son上找到它时就停止了,并且不会沿着原型链继续查找;这非常类似于嵌套标识符解析。所以我没有听说过它被那样使用,但是...我认为它可以工作,当然。 :-) 关于“...Foo.barSon.bar是两个独立的对象,而Foo.bar仍然在Son.bar的原型链中...”,我想你不是想在那里加上那些.bar,对吧? - T.J. Crowder
1
是的,对我来说已经太晚编辑了,但我的意思是要读作"Foo.barSon的原型链中",也就是说,如果你在一个可以使用super绕过自己的原型并向上查找的上下文中,Son可以访问Son.barFoo.bar - Coderer

11
在@T.J.Crowder的这个帖子的答案中详细解释了OP代码行为。
为了避免需要重新定义静态成员,您可以采取以下方法:
class Foo {
    private static bar = new Map<string, string[]>();

    public static addBar(bar: string) {
        let list: string[];

        if (this.bar.has(this.name)) {
            list = this.bar.get(this.name);
        } else {
            list = [];
            this.bar.set(this.name, list);
        }

        list.push(bar);
    }

    public static logBar() {
        console.log(this.bar.get(this.name));
    }
}

class Son extends Foo {}

class Daughter extends Foo {}

Foo.addBar('Hello');
Son.addBar('World');
Daughter.addBar('Both ?');

(code in playground)


2
请记住,Map 可以具有非字符串键,因此您可以直接使用构造函数作为键... :-) - T.J. Crowder
@T.J.Crowder 确实如此。但是除非我确实需要该实例(在这种情况下是构造函数)进行其他操作,否则我总是更喜欢使用“字符串ID”。这可能是我从其他语言中带来的偏好。 - Nitzan Tomer
1
@NitzanTomer 请确保配置您的缩小器不要混淆函数名称。我们曾经因此受到了影响。您应该使用构造函数引用,因为来自不同模块的两个类可能具有相同的名称。https://www.uglifyjs.net/#toggle_text_mangle_fnames - Ruan Mendes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接