如何实现打字稿装饰器?

时间:2015-04-21 14:55:55

标签: typescript decorator

TypeScript 1.5现在有decorators

有人可以提供一个简单的例子来演示实现装饰器的正确方法,并描述可能的有效装饰器签名中的参数是什么意思吗?

declare type ClassDecorator = <TFunction extends Function>(target: TFunction) => TFunction | void;
declare type PropertyDecorator = (target: Object, propertyKey: string | symbol) => void;
declare type MethodDecorator = <T>(target: Object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<T>) => TypedPropertyDescriptor<T> | void;
declare type ParameterDecorator = (target: Function, propertyKey: string | symbol, parameterIndex: number) => void;

此外,在实现装饰器时是否应该记住哪些最佳实践注意事项?

5 个答案:

答案 0 :(得分:371)

我最终玩弄装饰,并决定记录我想知道的任何想要在任何文档发布之前利用它的人。如果您发现任何错误,请随时编辑此内容。

一般要点

  • 在声明类时调用装饰器 - 而不是在实例化对象时调用。
  • 可以在相同的类/属性/方法/参数上定义多个装饰器。
  • 构造函数不允许使用装饰器。
  

有效的装饰者应该是:

     
      
  1. 可分配给其中一种装饰器类型(ClassDecorator | PropertyDecorator | MethodDecorator | ParameterDecorator)。
  2.   
  3. 返回一个可赋值给装饰值的值(在类装饰器和方法装饰器的情况下)。
  4.         

    Reference

方法/正式访问者装饰器

实施参数:

  • target:类的原型(Object)。
  • propertyKey:方法的名称(string | symbol)。
  • descriptorTypedPropertyDescriptor - 如果您不熟悉描述符的密钥,我建议您在Object.defineProperty class MyClass { @log myMethod(arg: string) { return "Message -- " + arg; } } 上阅读相关内容(这是第三个参数)。

示例 - 没有参数

使用:

function log(target: Object, propertyKey: string, descriptor: TypedPropertyDescriptor<any>) {
    const originalMethod = descriptor.value; // save a reference to the original method

    // NOTE: Do not use arrow syntax here. Use a function expression in 
    // order to use the correct value of `this` in this method (see notes below)
    descriptor.value = function(...args: any[]) {
        // pre
        console.log("The method args are: " + JSON.stringify(args));
        // run and store result
        const result = originalMethod.apply(this, args);
        // post
        console.log("The return value is: " + result);
        // return the result of the original method (or modify it before returning)
        return result;
    };

    return descriptor;
}

实现:

new MyClass().myMethod("testing");

输入:

@enumerable(false)

输出:

  

方法args是:[&#34; testing&#34;]

     

返回值为:消息 - 测试

注意:

  • 设置描述符的值时,请勿使用箭头语法。 this documentation
  • 修改原始描述符比通过返回新描述符覆盖当前描述符更好。这允许您使用多个装饰器来编辑描述符,而不会覆盖另一个装饰器所做的事情。这样做可以让您同时使用@logTypedPropertyDescriptor之类的内容(示例:The context of this will not be the instance's if you do. vs Bad
  • 有用class MyClass { @enumerable(false) get prop() { return true; } } function enumerable(isEnumerable: boolean) { return (target: Object, propertyKey: string, descriptor: TypedPropertyDescriptor<any>) => { descriptor.enumerable = isEnumerable; return descriptor; }; } 的类型参数可用于限制装饰器可以使用哪种方法签名(Good)或访问者签名(Method Example)穿上。

示例 - 使用参数(装饰工厂)

使用参数时,必须使用装饰器参数声明一个函数,然后返回一个带有示例签名且不带参数的函数。

target

静态方法装饰器

类似于具有一些差异的方法装饰器:

  • @isTestable class MyClass {} 参数是构造函数本身,而不是原型。
  • 描述符是在构造函数上定义的,而不是原型。

类装饰器

target

实施参数:

  • TFunction extends Function:在(class MyClass { @serialize name: string; } )上声明装饰器的类。

Accessor Example:使用元数据api存储类的信息。

Property Decorator

target

实施参数:

  • Object:类的原型(propertyKey)。
  • string:媒体资源的名称(@serialize("serializedName") | Example use)。

symbol:创建class MyClass { myMethod(@myDecorator myParameter: string) {} } 装饰器并将属性名称添加到要序列化的属性列表中。

参数装饰器

target

实施参数:

  • Function:该类的原型(Function - 似乎any不再有效。您应该使用ObjectpropertyKey这里现在为了在任何类中使用装饰器。或者指定要限制它的类类型)
  • string:方法的名称(parameterIndex | Example use)。
  • number:函数参数列表中的参数索引(daudio.src)。

symbol

详细示例

答案 1 :(得分:8)

我在其他答案中没有看到一件重要的事情:

装饰工厂

  

如果我们想要自定义装饰器如何应用于声明,我们可以编写一个装饰工厂。 Decorator Factory只是一个函数,它返回装饰器在运行时调用的表达式。

// This is a factory, returns one of ClassDecorator,
// PropertyDecorator, MethodDecorator, ParameterDecorator
function Entity(discriminator: string):  {
    return function(target) {
        // this is the decorator, in this case ClassDecorator.
    }
}

@Entity("cust")
export class MyCustomer { ... }

检查TypeScript手册Decorators chapter

答案 2 :(得分:4)

class Foo {
  @consoleLogger 
  Boo(name:string) { return "Hello, " + name }
}
  • 目标:在上述情况下的课程原型&#34; Foo&#34;
  • propertyKey:在上述情况下调用的方法的名称&#34; Boo&#34;
  • 描述符:对象的描述=&gt;包含value属性,它又是函数本身:function(name){return&#39; Hello&#39; +名字; }

您可以实现将每个调用记录到控制台的内容:

function consoleLogger(target: Function, key:string, value:any) 
{
  return value: (...args: any[]) => 
  {
     var a = args.map(a => JSON.stringify(a)).join();
     var result = value.value.apply(this, args);
     var r = JSON.stringify(result);

     console.log('called method' + key + ' with args ' + a + ' returned result ' + r);

     return result;
  }     
}

答案 3 :(得分:1)

TS装饰器:

TS装饰器允许在类上添加额外的功能。在创建类的任何实例之前,装饰者会在声明时间更改类。

语法:

装饰器以@符号声明,例如@metadata。 TS现在将搜索相应的元数据函数,并将自动为其提供sevaral参数,该参数随确切修饰的内容而异(例如,class或class属性获得不同的参数)

装饰器函数中提供了以下参数:

  • 该类的原型对象
  • 属性键或方法名称
  • PropertyDescriptor对象,如下所示 {writable: true, enumerable: false, configurable: true, value: ƒ}

根据装饰器的类型,这些参数中的1-3会传递给装饰器函数。

装饰器的类型:

以下修饰符可以应用于类,TS会按以下顺序对其进行评估(以下总和来自TS文档):

  1. 参数修饰符,后跟方法,访问器或属性修饰符将应用于每个实例成员。
  2. 参数修饰符,后跟方法,访问器或属性 装饰器应用于每个静态成员。
  3. 参数修饰符应用于构造函数。
  4. 班级装饰器应用于班级

更好地理解它们的最佳方法是通过示例。请注意,这些示例确实需要对TS语言和PropertyDescriptor之类的概念有深入的了解。

方法修饰符:

function overwrite(
    target: myClass,
    propertyKey: string,
    descriptor: PropertyDescriptor
) {
    console.log('I get logged when the class is declared!')

    // desciptor.value refers to the actual function fo the class
    // we are changing it to another function which straight up 
    // overrides the other function
    descriptor.value = function () {
        return 'newValue method overwritten'
    }
}

function enhance(
    target: myClass,
    propertyKey: string,
    descriptor: PropertyDescriptor
) {
    const oldFunc = descriptor.value;

    // desciptor.value refers to the actual function fo the class
    // we are changing it to another function which calls the old
    // function and does some extra stuff
    descriptor.value = function (...args: any[]) {
        console.log('log before');
        const returnValue = oldFunc.apply(this, args)
        console.log('log after');

        return returnValue;
    }
}


class myClass {

    // here is the decorator applied
    @overwrite
    foo() {
        return 'oldValue';
    }

    // here is the decorator applied
    @enhance
    bar() {
        return 'oldValueBar';
    }

}

const instance =new myClass()

console.log(instance.foo())
console.log(instance.bar())

// The following gets logged in this order:

//I get logged when the class is declared!
// newValue method overwritten
// log before
// log after
// oldValueBar

属性装饰器:

function metaData(
    target: myClass,
    propertyKey: string,
    // A Property Descriptor is not provided as an argument to a property decorator due to 
    // how property decorators are initialized in TypeScript.
) {

    console.log('Execute your custom code here')
    console.log(propertyKey)

}

class myClass {

    @metaData
    foo = 5

}


// The following gets logged in this order:

// Execute your custom code here
// foo

类修饰符(来自TS文档):

function seal(
    constructor: Function,
) {

    // Object.seal() does the following:
    // Prevents the modification of attributes of 
    // existing properties, and prevents the addition 
    // of new properties
    Object.seal(constructor);
    Object.seal(constructor.prototype);

}

@seal
class myClass {

    bar?: any;
    
    foo = 5

}
 
myClass.prototype.bar = 10;

// The following error will be thrown:

// Uncaught TypeError: Cannot add property bar,
// object is not extensible
 

装饰器和装饰器工厂:

可以通过装饰器函数或装饰器工厂函数声明

装饰器。语法上的差异最好通过一个示例来说明:

// Returns a decorator function, we can return any function
// based on argument if we want
function decoratorFactory(arg: string) {
    return function decorator(
    target: myClass,
    propertyKey: string,
) {
    console.log(`Log arg ${arg} in decorator factory`);
}
}

// Define a decorator function directly
function decorator(
    target: myClass,
    propertyKey: string,
) {
    console.log('Standard argument');
}

class myClass {

    // Note the parentheses and optional arguments 
    // in the decorator factory
    @decoratorFactory('myArgument')
    foo = 'foo';

    // No parentheses or arguments
    @decorator
    bar = 'bar';

}


// The following gets logged in this order:

// Log arg myArgument in decorator factory
// Standard argument

答案 4 :(得分:0)

您还可以为打字稿中的原始构造函数decorate/enhance(我使用3.9.7)提供新功能。下面的代码片段包装了原始构造函数,以为name属性添加前缀。这是在类为instantiated时发生的,而不是在类为declared时发生的!

 //Decorator function
 function Prefixer(prefix: string) { 
    return function<T extends { new (...args: any[]): {name: string} }>(
      originalCtor: T
    ) {
      return class extends originalCtor {
        constructor(..._: any[]) {
          super();
          this.name = `${prefix}.${this.name.toUpperCase()}`;        
          console.log(this.name);       
        }
      };
    };
  }

当类为instantiated时,新的构造函数逻辑将与原始ctor逻辑一起运行-

  @Prefixer('Mr')
  class Person {
    name = 'MBB';
  
    constructor() {
      console.log('original ctr logic here!');
    }
  }
  
  const pers = new Person();
  
  console.log(pers); //Mr.MBB