乐趣区

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

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

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

原文地址:https://github.com/forthealll…
欢迎 star
一、什么是装饰者模式
  最近在看 nestjs 等支持 Typescript 的 node 框架,经常看到这样一种写法:
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() 函数,返回相应的字符串。
  在上述的代码中,用 @Controller(‘cats’) 修饰 CatsController 类,通过 @Get 来修饰类中的 findAll 方法,这就是典型的装饰者模式。通过 @Controller(‘cats’) 和 @Get 修饰后的类 CatsController,简单来说,就是拥有了丰富的“内涵”。
下面看看具体装饰者模式的定义:
我们知道继承模式是丰富子元素“内涵”的一种重要方式,不管是继承接口还是子类继承基类。而装饰者模式可以在不改变继承关系的前提下,包装先有的模块,使其内涵更加丰富,并不会影响到原来的功能。与继承相比,更加的灵活。
javascript 中的装饰器处于建议征集的第二阶段,通过 babel 和 Typescrit 都可以实现装饰器的语法。
二、Typescript 中的装饰器
Typescript 中的装饰器与类相关,分别可以修饰类的实例函数和静态函数、类本身、类的属性、类中函数的参数以及类的 set/get 存取器,下面来意义介绍。
(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 中在基类中常用的装饰器后,我们发现:
装饰器可以起到分离复杂逻辑的功能,且使用上极其简单方便。与继承相比,也更加灵活,可以从装饰类,到装饰类函数的参数,可以说武装到了“牙齿”。
三、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”.
四、总结
通过装饰器,可以方便的修饰类,以及类的方法,类的属性等,相比于继承而言更加灵活,此外,通过注解的方法,可以在 Typescript 中引入元数据,实现元编程等。特别是在 angularjs、nestjs 中,大量使用了注解,特别是 nestjs 构建了类似于 java springMVC 式的 web 框架。

退出移动版