ts 装饰器

496 阅读6分钟

什么是装饰器

装饰器-DecoratorsTypeScript 中是一种可以在不修改类代码的基础上通过添加标注的方式来对类型进行扩展的一种方式

  • 提高代码复用率
  • 减少代码量
  • 提高代码扩展性、可读性和维护性

TypeScript 中,装饰器只能在类中使用

功能扩展

现有一个 H 类,实现了两个用于加、减的方法:addsub

// 原始类
class H {
    static add(a: number, b: number) {
        return a + b;
    }
    static sub(a: number, b: number) {
        return a - b;
    }
}

let v1 = H.add(1,2);
console.log(v1);
let v2 = H.sub(1,2);
console.log(v2);

需求:每次调用 addsub 的时候,同时保存(如:localStorage)参或者打印输出日志(如:console.log())与计算的数据与结果

// 原始类
class H {
    static add(a: number, b: number) {
      	let result = a + b;
      	log('add', a, b, result);
        return result;
    }
    static sub(a: number, b: number) {
		let result = a + b;
      	log('sub', a, b, result);
        return result;
    }
}

function storageData(type: string, a: number, b: number, result: number) {
  	console.log({
      type,
      a,
      b,
      result
    })
}

let v1 = H.add(1,2);
console.log(v1);
let v2 = H.sub(1,2);
console.log(v2);

上述方式虽然很快的实现了需求。但是,这样的做法对原有代码照成了破坏和侵入式的修改,不利于代码的维护。

storageData 抽离出来,通过 storageData 来包装方法

// 原始类
class H {
    static add(a: number, b: number) {
      	return a + b;
    }
    static sub(a: number, b: number) {
				return a - b;
    }
}

function storageData(fn: Function, type: string, a: number, b: number) {
  	let result = fn(a, b);
  	console.log({
      type,
      a,
      b,
      result
    })
  	return result;
}

let v1 = log(H.add, 'add', 1, 2);
console.log(v1);
let v2 = log(H.sub, 'sub', 1, 2);
console.log(v2);

这样做虽然可以避免去修改 addsub 方法,但是我们又得去大量的修改调用代码,怎样才能在不对代码进行修改也不对调用进行修改的同时来进行功能的扩展呢,这就是:装饰器 - Decorator

装饰器语法

装饰器的使用及其的简单

  • 装饰器本质就是一个函数,如上面的 storageData 就是
  • 通过特定语法在特定的位置调用装饰器函数即可对数据(类、方法、甚至参数等)进行扩展
// 装饰器函数
function log(target: Function, type: string, descriptor: PropertyDescriptor) {
    let value = descriptor.value;

    descriptor.value = function(a: number, b: number) {
        let result = value(a, b);
        console.log('日志:', {
            type,
            a,
            b,
            result
        })
        return result;
    }
}

// 原始类
class H {
    @log
    static add(a: number, b: number) {
        return a + b;
    }
    @log
    static sub(a: number, b: number) {
        return a - b;
    }
}

let v1 = H.add(1, 2);
console.log(v1);
let v2 = H.sub(1, 2);
console.log(v2);

装饰器

装饰器 是一个函数,它可以通过 @装饰器函数 这种特殊的语法附加在 方法访问符属性参数 上,对它们进行包装,然后返回一个包装后的目标对象(方法访问符属性参数 ),装饰器工作在类的构建阶段,而不是使用阶段

function 装饰器1() {}
...

@装饰器1
class MyClass {
  
  private _x: number;
  
  @装饰器2
  property1: number;
  
  @装饰器3
  get x() { return this._x; }
  
  @装饰器4
  public method1(@装饰器5 x: number) {
    //
  }
}

类装饰器

目标

  • 应用于类的构造函数

参数

  • 第一个参数(也只有一个参数)
    • 类的构造函数作为其唯一的参数

方法装饰器

目标

  • 应用于类的方法上

参数

  • 第一个参数
    • 静态方法:类的构造函数
    • 实例方法:类的原型对象
  • 第二个参数
    • 方法名称
  • 第三个参数
    • 方法描述符对象

属性装饰器

目标

  • 应用于类的属性上

参数

  • 第一个参数
    • 静态方法:类的构造函数
    • 实例方法:类的原型对象
  • 第二个参数
    • 属性名称

访问器装饰器

目标

  • 应用于类的访问器(getter、setter)上

参数

  • 第一个参数
    • 静态方法:类的构造函数
    • 实例方法:类的原型对象
  • 第二个参数
    • 属性名称
  • 第三个参数
    • 方法描述符对象

参数装饰器

目标

  • 应用在参数上

参数

  • 第一个参数
    • 静态方法:类的构造函数
    • 实例方法:类的原型对象
  • 第二个参数
    • 方法名称
  • 第三个参数
    • 参数在函数参数列表中的索引

装饰器执行顺序

实例装饰器

​ 属性 => 访问符 => 参数 => 方法

静态装饰器

​ 属性 => 访问符 => 参数 => 方法

​ 类

装饰器工厂

如果我们需要给装饰器执行过程中传入一些参数的时候,就可以使用装饰器工厂来实现

// 装饰器函数
function log(callback: Function) {
  	return function(target: Function, type: string, descriptor: PropertyDescriptor) {
     	 	let value = descriptor.value;

        descriptor.value = function(a: number, b: number) {
            let result = value(a, b);
            callback({
                type,
                a,
                b,
                result
            });
            return result;
        }
    }
}

// 原始类
class H {
    @log(function(result: any) {
      	console.log('日志:', result)
    })
    static add(a: number, b: number) {
        return a + b;
    }
    @log(function(result: any) {
      	localStorage.setItem('log', JSON.stringify(result));
    })
    static sub(a: number, b: number) {
        return a - b;
    }
}

let v1 = H.add(1, 2);
console.log(v1);
let v2 = H.sub(1, 2);
console.log(v2);

元数据

装饰器 函数中 ,我们可以拿到 方法访问符属性参数 的基本信息,如它们的名称,描述符 等,但是我们想获取更多信息就需要通过另外的方式来进行:元数据

什么是元数据?

元数据 :用来描述数据的数据,在我们的程序中,对象 等都是数据,它们描述了某种数据,另外还有一种数据,它可以用来描述 对象,这些用来描述数据的数据就是 元数据

比如一首歌曲本身就是一组数据,同时还有一组用来描述歌曲的歌手、格式、时长的数据,那么这组数据就是歌曲数据的元数据

使用 reflect-metadata

www.npmjs.com/package/ref…

首先,需要安装 reflect-metadata

npm install reflect-metadata

定义元数据

我们可以 方法 等数据定义元数据

  • 元数据会被附加到指定的 方法 等数据之上,但是又不会影响 方法 本身的代码

设置

Reflect.defineMetadata(metadataKey, metadataValue, target, propertyKey)

  • metadataKey:meta 数据的 key
  • metadataValue:meta 数据的 值
  • target:meta 数据附加的目标
  • propertyKey:对应的 property key

调用方式

  • 通过 Reflect.defineMetadata 方法调用来添加 元数据

  • 通过 @Reflect.metadata 装饰器来添加 元数据

@Reflect.metadata("name", '我是A类')
class A {
    @Reflect.metadata("name1", "val1")
    public method1() {
    }
  
  	@Reflect.metadata("name2", "val2")
  	public method2() {
    }
}

// or
Reflect.defineMetadata("name", "我是A类", A);
Reflect.defineMetadata("name1", "val1", new A, 'method1');
Reflect.defineMetadata("name2", "val2", new A, 'method2');

获取

Reflect.getMetadata(metadataKey, target, propertyKey)

参数的含义与 defineMetadata 对应

使用 emitDecoratorMetadata

tsconfig.json 中有一个配置 emitDecoratorMetadata,开启该特性,typescript 会在编译之后自动给 方法访问符属性参数 添加如下几个元数据

  • design:type:被装饰目标的类型
    • 成员属性:属性的标注类型
    • 成员方法:Function 类型
  • design:paramtypes
    • 成员方法:方法形参列表的标注类型
    • 类:构造函数形参列表的标注类型
  • design:returntype
    • 成员方法:函数返回值的标注类型
import "reflect-metadata"

function n(target: any) {
}
function f(name: string) {
    return function(target: any, propertyKey: string, descriptor: any) {
      	console.log( 'design type', Reflect.getMetadata('design:type', target, propertyKey) );
        console.log( 'params type', Reflect.getMetadata('design:paramtypes', target, propertyKey) );
        console.log( 'return type', Reflect.getMetadata('design:returntype', target, propertyKey) );
    }
}
function m(target: any, propertyKey: string) {

}

@n
class B {
    @m
    name: string;

    constructor(a: string) {

    }

    @f('')
    method1(a: string, b: string) {
        return 'a'
    }
}

编译后

__decorate([
    m,
    __metadata("design:type", String)
], B.prototype, "name", void 0);
__decorate([
    f(''),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [String, String]),
    __metadata("design:returntype", void 0)
], B.prototype, "method1", null);
B = __decorate([
    n,
    __metadata("design:paramtypes", [String])
], B);