为什么基类上的TypeScript装饰器会导致扩展相互影响?

时间:2018-01-05 22:42:52

标签: typescript decorator

假设您有一个装饰器来跟踪它装饰的字段:

interface FieldTracker {
    fields: string[];
}

const decorator = <T extends FieldTracker>(target: T, fieldName: string) => {
    target.fields = target.fields || [];
    target.fields.push(fieldName);
};

然后假设您创建了一个使用该装饰器的抽象Base类:

abstract class Base implements FieldTracker {
    fields: string[];

    @decorator
    a: string = 'a';
}

然后创建两个扩展Base类的类。

class FirstClass extends Base {
    @decorator
    b: string = 'b';
}

class SecondClass extends Base {
    @decorator
    c: string = 'c';
}

在实例化SecondClass后,其fields将包含FirstClass中装饰的字段:

const secondInstance = new SecondClass();

expect(secondInstance.fields).toEqual(['a', 'c']);

这导致:

Expected value to equal:
  ["a", "c"]
Received:
  ["a", "b", "c"]

一些观察

  • 如果我将参数记录到decorator,我会得到:

    1. target: Base {}, fieldName: 'a'
    2. target: FirstClass {}, fieldName: 'b'
    3. target: SecondClass {}, fieldName: 'c'
  • 请注意,Baseabstract,无法实例化。 target如何成为它的一个实例?

  • 请注意,我从未实例化过FirstClasstarget如何成为它的一个实例?

  • 如果我在decorator上不使用Base,则不会发生这种情况。这意味着fields位于Base.prototype,似乎不应该存在。

1 个答案:

答案 0 :(得分:4)

这里有点混乱。

  • abstract类的Base性质意味着如果您尝试使用它直接构造Base的实例,编译器会对您大喊大叫。它仍然具有类构造函数的所有装置,包括拥有prototype。如果您在the TypeScript Playground检查代码的发出JavaScript,可以看到这一点。

  • 装饰器作用于每个类构造函数的prototype(因此当你装饰Base时,它正在修改Base.prototype)。它不是(直接)代表该类的任何实例。装饰器只为你装饰的每个类调用一次。

  • 子类inherits fromprototype超类的prototype。这样,子类实例的原型链包括子类构造函数prototype以及超类构造函数prototype

  • 如果将数组分配给变量,则不会复制数组的内容;只有一个数组对象,从一个变量对它做出的任何更改都将在另一个变量中可见。

说了这么多,让我们检查你的装饰者:

const decorator = <T extends FieldTracker>(target: T, fieldName: string) => {
    target.fields = target.fields || [];  // hmm
    target.fields.push(fieldName);
};

在标有// hmm的行中,您正在检查其prototype属性的传入fields对象。对于Base.prototype,这不会存在,并将初始化为新的空数组。对于FirstClass.prototype,这不会直接找到,但由于FirstClass.prototype继承自Base.prototype,因此会在那里找到它。通过将FirstClass.prototype.fields设置为Base.prototype.fields,您可以将属性直接添加到FirstClass.prototype,但该值与Base.prototype上的数组对象相同。将值推送到FirstClass.prototype.fields时,您会在Base.prototype.fields上看到更改。类似于SecondClass.prototype.fields

这意味着您会遇到不良行为:

console.log(Base.prototype.fields);  // Array [ "a", "b", "c" ]
console.log(FirstClass.prototype.fields); // Array [ "a", "b", "c" ]
console.log(SecondClass.prototype.fields); // Array [ "a", "b", "c" ]

对此的修复非常简单;不要复制数组引用,但要将其内容复制到新数组。最简单的方法是使用原始数组&#39; slice() method

const decorator = <T extends FieldTracker>(target: T, fieldName: string) => {
    target.fields = (target.fields || []).slice();  // all better
    target.fields.push(fieldName);
};

现在,如果你运行上面的代码,测试应该通过。具体来说,您应该看到:

console.log(Base.prototype.fields);  // Array [ "a" ]
console.log(FirstClass.prototype.fields); // Array [ "a", "b" ]
console.log(SecondClass.prototype.fields); // Array [ "a", "c" ]

希望有所帮助;祝你好运!