带有继承的打字稿装饰器
Posted
技术标签:
【中文标题】带有继承的打字稿装饰器【英文标题】:Typescript decorators with inheritance 【发布时间】:2017-10-10 06:21:05 【问题描述】:我正在使用 Typescript 装饰器,它们的行为似乎与我在与类继承一起使用时所期望的完全不同。
假设我有以下代码:
class A
@f()
propA;
class B extends A
@f()
propB;
class C extends A
@f()
propC;
function f()
return (target, key) =>
if (!target.test) target.test = [];
target.test.push(key);
;
let b = new B();
let c = new C();
console.log(b['test'], c['test']);
哪些输出:
[ 'propA', 'propB', 'propC' ] [ 'propA', 'propB', 'propC' ]
虽然我希望这样:
[ 'propA', 'propB' ] [ 'propA', 'propC' ]
所以,target.test
似乎在 A、B 和 C 之间共享。我对这里发生的事情的理解如下:
-
由于 B 扩展了 A,
new B()
首先触发了 A 的实例化,这触发了对 A 的 f
的评估。由于 target.test
未定义,因此将其初始化。
f
然后为 B 评估,因为它扩展了 A,所以首先实例化 A。所以,当时target.test
(target
是B)引用了为A定义的test
。所以,我们将propB
推入其中。至此,一切按预期进行。
与第 2 步相同,但对于 C。这一次,当 C 评估装饰器时,我希望它有一个新对象 test
,不同于为 B 定义的对象。但日志证明我错了。
谁能向我解释为什么会发生这种情况 (1) 以及我将如何实现 f
以使 A 和 B 具有单独的 test
属性?
我猜你会称它为“特定于实例”的装饰器?
【问题讨论】:
【参考方案1】:好的,所以在花了几个小时在网上玩了几个小时后,我得到了一个工作版本。我不明白为什么这是有效的,所以请原谅缺乏解释。
关键是使用Object.getOwnPropertyDescriptor(target, 'test') == null
而不是!target.test
来检查test
属性是否存在。
如果你使用:
function f()
return (target, key) =>
if (Object.getOwnPropertyDescriptor(target, 'test') == null) target.test = [];
target.test.push(key);
;
控制台将显示:
[ 'propB' ] [ 'propC' ]
这几乎是我想要的。现在,该数组特定于每个实例。但这意味着数组中缺少 'propA'
,因为它是在 A 中定义的。因此我们需要访问父目标并从那里获取属性。我花了一段时间才弄明白,但你可以通过Object.getPrototypeOf(target)
得到它。
最终的解决方案是:
function f()
return (target, key) =>
if (Object.getOwnPropertyDescriptor(target, 'test') == null) target.test = [];
target.test.push(key);
/*
* Since target is now specific to, append properties defined in parent.
*/
let parentTarget = Object.getPrototypeOf(target);
let parentData = parentTarget.test;
if (parentData)
parentData.forEach(val =>
if (target.test.find(v => v == val) == null) target.test.push(val);
);
;
哪些输出
[ 'propB', 'propA' ] [ 'propC', 'propA' ]
任何人都可以理解为什么这是有效的,而上述内容并没有启发我。
【讨论】:
【参考方案2】:我认为这是因为当创建类 B 时,A 的原型被复制了它的所有自定义属性(作为引用)。
如果 C 类没有任何装饰器,我使用稍微修改的解决方案,接缝来解决更自然的重复问题。
仍然不确定这是否是处理此类情况的最佳方法:
function foo(target, key)
let
ctor = target.constructor;
if (!Object.getOwnPropertyDescriptor(ctor, "props"))
if (ctor.props)
ctor.props = [...ctor.props];
else
ctor.props = [];
ctor.props.push(key);
abstract class A
@foo
propA = 0;
class B extends A
@foo
propB = 0;
class C extends A
@foo
propC = 0;
【讨论】:
【参考方案3】:@user5365075 我在使用方法装饰器时遇到了完全相同的问题,您的修复成功了。
这是我在我的一个包中使用的装饰器(使用对象而不是数组):
export function property(options)
return (target, name) =>
// Note: This is a workaround due to a similar bug described here:
// https://***.com/questions/43912168/typescript-decorators-with-inheritance
if (!Object.getOwnPropertyDescriptor(target, '_sqtMetadata'))
target._sqtMetadata =
if (target._sqtMetadata.properties)
target._sqtMetadata.properties[name] = options.type
else
target._sqtMetadata.properties = [name]: options.type
const parentTarget = Object.getPrototypeOf(target)
const parentData = parentTarget._sqtMetadata
if (parentData)
if (parentData.properties)
Object.keys(parentData.properties).forEach((key) =>
if (!target._sqtMetadata.properties[key])
target._sqtMetadata.properties[key] = parentData.properties[key]
)
我可以确认类装饰器也存在相同的行为。
【讨论】:
【参考方案4】:您的代码具有这种行为,因为您的装饰字段是实例成员,您收到的 target
是该类的原型。执行开始时,将首先加载类A
,因为它是父类。所以test
数组设置在A 类的prototype
上,它由所有子类B/C 共享。因此,您会在 test
数组中看到 3 个元素。
不使用getOwnPropertyDescriptor()
,另一种方法是在类本身的target.constructor
上注册元。然后每个类将拥有自己的元数据,在收集装饰字段时,您只需搜索原型链并将它们全部收集。 (使用标准的relect-metadata
作为助手)。
function f()
return (target, key) =>
if (!Reflect.hasOwnMetadata('MySpecialKey', target.constructor))
// put field list on the class.
Reflect.defineMetadata('MySpecialKey', [], target.constructor);
Reflect.getOwnMetadata('MySpecialKey', target.constructor).push(key);
;
/**
* @param clz the class/constructor
* @returns the fields decorated with @f all the way up the prototype chain.
*/
static getAllFields(clz: Record<string, any>): string[]
if(!clz) return [];
const fields: string[] | undefined = Reflect.getMetadata('MySpecialKey', clz);
// get `__proto__` and (recursively) all parent classes
const rs = new Set([...(fields || []), ...this.getAllFields(Object.getPrototypeOf(clz))]);
return Array.from(rs);
另一个选项是使用类验证器方式,它拥有一个包含所有装饰器相关信息的全局元数据存储。在执行逻辑时,检查是否the target constructor is instanceof the registered target。如果是,请包含该字段。
【讨论】:
以上是关于带有继承的打字稿装饰器的主要内容,如果未能解决你的问题,请参考以下文章