聊聊Typescript中的设计模式——装饰器篇(decorators)


  随着Typescript的普及,在KOA2和nestjs等nodejs框架中常常看到相似于java spring中注解的写法。本文从装饰模式出发,聊聊Typescipt中的装饰器和注解。javascript

  • 什么是装饰者模式
  • Typescript中的装饰器
  • Typescript中的注解
  • 总结

原文地址在:github.com/fortheallli…java

欢迎starnode

1、什么是装饰者模式

  最近在看nestjs等支持Typescript的node框架,常常看到这样一种写法:git

import { Controller, Get } from '@nestjs/common';

@Controller('cats')

export class CatsController {
  @Get()
  findAll() {
    return 'This action returns all cats';
  }
}
复制代码

  上述代码定义了一个处理url为“/cats”的控制器,该控制器对于url为“/cats”的get方法执行findAll()函数,返回相应的字符串。angularjs

  在上述的代码中,用@Controller('cats')修饰CatsController类,经过@Get来修饰类中的findAll方法,这就是典型的装饰者模式。经过@Controller('cats')和@Get修饰后的类CatsController,简单来讲,就是拥有了丰富的“内涵”。github

下面看看具体装饰者模式的定义:web

咱们知道继承模式是丰富子元素“内涵”的一种重要方式,无论是继承接口仍是子类继承基类。而装饰者模式能够在不改变继承关系的前提下,包装先有的模块,使其内涵更加丰富,并不会影响到原来的功能。与继承相比,更加的灵活。spring

javascript中的装饰器处于建议征集的第二阶段,经过babel和Typescrit均可以实现装饰器的语法。编程

2、Typescript中的装饰器

Typescript中的装饰器与类相关,分别能够修饰类的实例函数和静态函数、类自己、类的属性、类中函数的参数以及类的set/get存取器,下面来意义介绍。babel

(1)、类方法的装饰器

下面来介绍一下用装饰器来修饰函数,首先来看一个例子:

let temple;
function log(target, key, descriptor) {
  console.log(`${key} was called!`);
  temple = target;
}
class P {
    @log
    foo() {
      console.log('Do something');
    }
}

const p = new P()
p.foo()
console.log(P.prototype === temple) //true
复制代码

上述是实例方法foo中咱们用log函数修饰,log函数接受三个参数,经过P.prototype === temple(target)能够判断,在类的实例函数的装饰器函数第一个参数为类的原型,第二个参数为函数名自己,第三个参数为该函数的描述属性。

具体总结以下,对于类的函数的装饰器函数,依次接受的参数为:

  • target:若是修饰的是类的实例函数,那么target就是类的原型。若是修饰的是类的静态函数,那么target就是类自己。
  • key: 该函数的函数名。
  • descriptor:该函数的描述属性,好比 configurable、value、enumerable等。

从上述的例子中咱们能够看到,用装饰器来修饰相应的类的函数十分方便:

@log
foo() {
  ...
}
复制代码

(2)、类的装饰器

装饰函数也能够直接修饰类:

let temple
function foo(target){
   console.log(target);
   temple = target
}
@foo
class P{
   constructor(){
     
   }
}

const p = new P();
temple === P //true
复制代码

当装饰函数直接修饰类的时候,装饰函数接受惟一的参数,这个参数就是该被修饰类自己。上述的例子中,输出的target就是类P的自己。

此外,在修饰类的时候,若是装饰函数有返回值,该返回值会从新定义这个类,也就是说当装饰函数有返回值时,实际上是生成了一个新类,该新类经过返回值来定义。

举例来讲:

function foo(target){
   return class extends target{
      name = 'Jony';
      sayHello(){
         console.log("Hello "+ this.name)
      }
   }
}
@foo
class P{
   constructor(){
     
   }
}

const p = new P();
p.sayHello(); // 会输出Hello Jony
复制代码

上面的例子能够看到,当装饰函数foo有返回值时,实际上P类已经被返回值所表明的新类所代替,所以P的实例p拥有sayHello方法。

(3)、类的属性的装饰器

下面咱们来看类的属性的装饰器,装饰函数修饰类的属性时,在类实例化的时候调用属性的装饰函数,举例来讲:

function foo(target,name){
   console.log("target is",target);
   console.log("name is",name)
}
class P{
   @foo
   name = 'Jony'
}
const p = new P();
//会依次输出 target is f P()  name is Jony
复制代码

这里对于类的属性的装饰器函数接受两个参数,对于静态属性而言,第一个参数是类自己,对于实例属性而言,第一个参数是类的原型,第二个参数是指属性的名字。

(4)、类函数参数的装饰器

接着来看类函数参数的装饰器,类函数的参数装饰器能够修饰类的构建函数中的参数,以及类中其余普通函数中的参数。该装饰器在类的方法被调用的时候执行,下面来看实例:

function foo(target,key,index){
   console.log("target is",target);
   console.log("key is",key);
   console.log("index is",index)
}
class P{
   test(@foo a){
   }
}
const p = new P();
p.test("Hello Jony")
// 依次输出 f P() , test , 0 
复制代码

类函数参数的装饰器函数接受三个参数,依次为类自己,类中该被修饰的函数自己,以及被修饰的参数在参数列表中的索引值。上述的例子中,会依次输出 f P() 、test和0。再次明确一下修饰函数参数的装饰器函数中的参数含义:

  • target: 类自己
  • key:该参数所在的函数的函数名
  • index: 该参数在函数参数列表中的索引值

从上面的Typescrit中在基类中经常使用的装饰器后,咱们发现:

装饰器能够起到分离复杂逻辑的功能,且使用上极其简单方便。与继承相比,也更加灵活,能够从装饰类,到装饰类函数的参数,能够说武装到了“牙齿”。

3、Typescript中的注解

在了解了Typescrit中的装饰器以后,接着咱们来看Typescrit中的注解。

什么是注解,所谓注解的定义就是:

为相应的类附加元数据支持

所谓元数据能够简单的解释,就是修饰数据的数据,好比一我的有name,age等数据属性,那么name和age这些字段就是为了修饰数据的数据,能够简单的称为元数据。

元数据简单来讲就是能够修饰某些数据的字段。下面给出装饰器和注解的解释和区别:

  • 装饰器:定义劫持,能够对类,类的方法,类的属性以及类的方法的入参进行修改。不提供元数据的支持。
  • 注解:仅提供元数据的支持。

二者之间的联系:

经过注解添加元数据,而后在装饰器中获取这些元数据,完成对类、类的方法等等的修改,能够在装饰器中添加元数据的支持,好比能够能够在装饰器工厂函数以及装饰器函数中添加元数据支持等

(1)、Typescript中的元数据操做

能够经过reflect-metadata包来实现对于元数据的操做。首先咱们来看reflect-metadata的使用,首先定义使用元数据的函数:

const formatMetadataKey = Symbol("format");

function format(formatString: string) {
    return Reflect.metadata(formatMetadataKey, formatString);
}

function getFormat(target: any, propertyKey: string) {
    return Reflect.getMetadata(formatMetadataKey, target, propertyKey);
}
复制代码

这里的format能够做为装饰器函数的工厂函数,由于format函数返回的是一个装饰器函数,上述的方法定义了元数据Sysmbol("format"),用Sysmbol的缘由是为了防止元数据中的字段重复,而format定义了取元数据中相应字段的功能。

接着咱们来在类中使用相应的元数据:

class Greeter {
    @format("Hello, %s")
    name: string;

    constructor(name: string) {
        this.name = message;
    }
    sayHello() {
        let formatString = getFormat(this, "name");
        return formatString.replace("%s", this.name);
    }
}

const g = new Greeter("Jony");
console.log(g.sayHello());
复制代码

在上述中,咱们在name属性的装饰器工厂函数,执行@format("Hello, %s"),返回一个装饰器函数,且该装饰器函数修饰了Greeter类的name属性,将“name”属性的值写入为"Hello, %s"。

而后再sayHello方法中,经过getFormat(this,"name")取到formatString为“Hello,%s”.

4、总结

经过装饰器,能够方便的修饰类,以及类的方法,类的属性等,相比于继承而言更加灵活,此外,经过注解的方法,能够在Typescript中引入元数据,实现元编程等。特别是在angularjs、nestjs中,大量使用了注解,特别是nestjs构建了相似于java springMVC式的web框架。

相关文章
相关标签/搜索