【问题标题】: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 之间共享。我对这里发生的事情的理解如下:

  1. 由于 B 扩展了 A,new B() 首先触发了 A 的实例化,这触发了对 A 的 f 的评估。由于 target.test 未定义,因此将其初始化。
  2. f 然后为 B 评估,因为它扩展了 A,所以首先实例化 A。所以,当时target.testtarget是B)引用了为A定义的test。所以,我们将propB推入其中。至此,一切按预期进行。
  3. 与第 2 步相同,但对于 C。这一次,当 C 评估装饰器时,我希望它有一个新对象 test,不同于为 B 定义的对象。但日志证明我错了。

谁能向我解释为什么会发生这种情况 (1) 以及我将如何实现 f 以使 A 和 B 具有单独的 test 属性?

我猜你会称它为“特定于实例”的装饰器?

【问题讨论】:

    标签: typescript inheritance decorator


    【解决方案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://stackoverflow.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。如果是,请包含该字段。

          【讨论】:

            猜你喜欢
            • 2016-12-05
            • 2020-03-28
            • 2018-09-17
            • 2017-12-04
            • 2018-06-21
            • 2019-07-29
            • 2016-05-08
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多