具有继承的Typescript装饰器

use*_*075 5 inheritance decorator typescript

我在玩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']);
Run Code Online (Sandbox Code Playgroud)

哪个输出:

[ 'propA', 'propB', 'propC' ] [ 'propA', 'propB', 'propC' ]
Run Code Online (Sandbox Code Playgroud)

虽然我期望这样:

[ 'propA', 'propB' ] [ 'propA', 'propC' ]
Run Code Online (Sandbox Code Playgroud)

因此,似乎target.test在A,B和C之间共享。我对这里发生的情况的理解如下:

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

谁能向我解释为什么会这样(1)以及如何实现fA和B具有单独的test属性?

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

use*_*075 5

好了,所以花了几个小时玩转并搜索网络后,我得到了一个有效的版本。我不明白为什么这样做有效,因此请原谅缺乏解释。

关键是使用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);
    };
}
Run Code Online (Sandbox Code Playgroud)

控制台将显示:

[ 'propB' ] [ 'propC' ]
Run Code Online (Sandbox Code Playgroud)

这几乎是我想要的。现在,该数组特定于每个实例。但这意味着'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);
            });
        }
    };
}
Run Code Online (Sandbox Code Playgroud)

哪个输出

[ 'propB', 'propA' ] [ 'propC', 'propA' ]
Run Code Online (Sandbox Code Playgroud)

愿任何了解上述原因的人无法启发我。


LeO*_* Li 5

您的代码具有这种行为,因为您的装饰字段是实例成员,target您收到的是该类的原型。当执行开始时,类A将首先加载,因为它是父类。因此该test数组设置在类 A 上,由所有子类B/Cprototype共享。因此您会在数组中看到 3 个元素。test

一种替代方法是在类本身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);
  }


Run Code Online (Sandbox Code Playgroud)

另一种选择是使用类验证器方式,它具有包含所有装饰器相关信息的全局元数据存储。在执行逻辑时,检查目标构造函数是否是注册目标的实例。如果是这样,请包括该字段。