带有继承的打字稿装饰器

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.testtarget是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。如果是,请包含该字段。

【讨论】:

以上是关于带有继承的打字稿装饰器的主要内容,如果未能解决你的问题,请参考以下文章

带有箭头功能的打字稿装饰器

打字稿装饰混乱

是否可以模拟打字稿装饰器?

打字稿方法返回未定义的方法装饰器

打字稿装饰器不能使用箭头函数

Vue Prop 未定义,和/或不能在 v-for 上使用。使用打字稿和装饰器