假设您有一个装饰器来跟踪它装饰的字段:
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
,我会得到:
target: Base {}, fieldName: 'a'
target: FirstClass {}, fieldName: 'b'
target: SecondClass {}, fieldName: 'c'
请注意,Base
为abstract
,无法实例化。 target
如何成为它的一个实例?
请注意,我从未实例化过FirstClass
。 target
如何成为它的一个实例?
如果我在decorator
上不使用Base
,则不会发生这种情况。这意味着fields
位于Base.prototype
,似乎不应该存在。
答案 0 :(得分:4)
这里有点混乱。
abstract
类的Base
性质意味着如果您尝试使用它直接构造Base
的实例,编译器会对您大喊大叫。它仍然具有类构造函数的所有装置,包括拥有prototype
。如果您在the TypeScript Playground检查代码的发出JavaScript,可以看到这一点。
装饰器作用于每个类构造函数的prototype
(因此当你装饰Base
时,它正在修改Base.prototype
)。它不是(直接)代表该类的任何实例。装饰器只为你装饰的每个类调用一次。
子类inherits from的prototype
超类的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" ]
希望有所帮助;祝你好运!