概述

装饰器(decorator)是面向对象的概念(在java中叫注解,c#中叫特征)

angular大量使用,react中也会用到,之前前端用的很少,目前越来越多

JS支持装饰器,但是目前不是最终阶段,还未成为标准

在TS中使用装饰器,需要开启 experimentalDecorators 配置

解决的问题

装饰器能够带来额外的信息量,可以达到分离关注点的目的。

比如有个User类,它里面的很多属性,需要进行规则校验,怎么做合适呢

  1. 把所有校验逻辑抽离到一个单独的函数内,即便让其成为类的成员方法,也有缺陷
    1. 我们是写属性的时候最清楚这个属性想表达什么,校验规则是啥;而不是在函数里一一处理
    2. 虽然对于重复代码可以一直抽离为新的函数,但是这样代码仍然多,函数很多

关注点的问题:

  • 在定义某个东西时,应该最清楚该东西的情况,而不是在其他时刻再去根据我们自己的判断写一次代码
    • 简述为:信息书写位置的问题
  • 重复代码/过多函数问题

上述两个问题产生的根源:某些信息,在定义时,能够附加的信息量有限。导致虽然我们清楚这些附加信息(比如我们写的注释的信息),但是编程语言、计算机它不知道啊!

装饰器的作用:为某些属性、类、参数、方法提供元数据(metadata)信息
元数据:描述数据 的 数据/信息

装饰器的本质

在JS中,装饰器本质上是一个函数。(装饰器是要参与运行的!)
装饰器可以修饰:类/成员(属性+方法)/参数

类装饰器

类装饰器的本质

记住!它必须是一个函数,该函数必须接收一个参数,该必须参数表示类本身(构造函数本身)
使用装饰器: @函数名

在TS中如何约束一个变量为类?

  • target: Function
    • 但是不能new target了,因为TS不知道你到底是类还是普通函数
  • target: new (…args: any[]) => object

    • 这样就表示target是一个类!且可以传constructor任意的参数

      类装饰器函数运行的时间

      在类定义后立即运行

      类装饰器可以具有的返回值

  • void:仅运行函数

  • 返回一个新的类:会将新的类替换掉装饰目标,所以这里要小心啊,一般是用于加强功能,取继承即可
    • 即便用继承,仍有危险,因为返回的儿子类没有类型检查!本质是因为target是动态的,无法确定

      想给装饰器传其他参数怎么办?

      ```typescript type constructor = new (…args: any[]) => object

function d1(name: string){ console.log(‘这个d1是装饰器的包装层, 普通函数’) return function (target: constructor){ console.log(‘这个函数才是真正的装饰器’) } }

@d1(‘wjw’)// 这里这个d1(‘wjw’)只是个普通的函数 class A { prop1: string }

  1. <a name="vHLtA"></a>
  2. ## 多个类装饰器函数运行顺序
  3. **真正返回的类装饰器函数是从下至上运行,但是包装层即普通函数是正常的从上至下顺序**<br />**如果还有方法和属性装饰器的包装层,则先执行他们,最后再执行类装饰器的包装层!**
  4. ```typescript
  5. type constructor = new (...args: any[]) => object
  6. function d1(name: string){
  7. console.log('1. 这个d1是装饰器的包装层')
  8. return function (target: constructor){
  9. console.log('4. 这个函数才是真正的d1装饰器')
  10. }
  11. }
  12. function d2(name: string){
  13. console.log('2. 这个d2是装饰器的包装层')
  14. return function (target: constructor){
  15. console.log('3. 这个函数才是真正的d2装饰器')
  16. }
  17. }
  18. @d1('wjw')
  19. @d2('wqq')
  20. class A {
  21. prop1: string
  22. constructor(){
  23. this.prop1 = 'wjw'
  24. console.log('A')
  25. }
  26. }

编译后的结果:

  1. var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
  2. var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
  3. if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
  4. else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
  5. return c > 3 && r && Object.defineProperty(target, key, r), r;
  6. };
  7. function d1(name) {
  8. console.log('1. 这个d1是装饰器的包装层');
  9. return function (target) {
  10. console.log('4. 这个函数才是真正的d1装饰器');
  11. };
  12. }
  13. function d2(name) {
  14. console.log('2. 这个d2是装饰器的包装层');
  15. return function (target) {
  16. console.log('3. 这个函数才是真正的d2装饰器');
  17. };
  18. }
  19. let A = class A {
  20. constructor() {
  21. this.prop1 = 'wjw';
  22. console.log('A');
  23. }
  24. };
  25. A = __decorate([
  26. d1('wjw'),
  27. d2('wqq')
  28. ], A);

成员装饰器

均可以有多个装饰器修饰

属性装饰器

属性装饰器也是一个函数,该函数需要两个参数

  1. 如果是静态属性,则为类本身。如果是实例属性,则为类的原型
  2. 固定为一个字符串,表示属性名

    方法装饰器

    同样,方法装饰器也是一个函数,该函数需要三个参数

  3. 同上

  4. 固定为一个字符串,表示方法名
  5. 属性描述对象,也是Object.defineProperty的第三个参数

练习:类和属性的描述装饰器

  1. function printObj(obj: any) {
  2. if(obj.$classDescriptor){
  3. console.log(obj.$classDescriptor);
  4. }else{
  5. console.log(obj.__proto__.constructor.name);
  6. }
  7. if(obj.$propDescriptions){
  8. console.log(obj.$propDescriptions);
  9. }else{
  10. obj.$propDescriptions = [];
  11. }
  12. for(const key in obj){
  13. if(obj.hasOwnProperty(key)){
  14. const prop = obj.$propDescriptions.find((p: any) => p.propName === key)
  15. if(prop){
  16. console.log(`\t${prop.description}: ${obj[key]}`)
  17. }else{
  18. console.log(`\t${key}: ${obj[key]}`)
  19. }
  20. }
  21. }
  22. }
  23. type construct = new (...args: any[]) => object;
  24. function classDescriptor(description: string) {
  25. return function (target: Function) {
  26. // 保存到该类到原型中
  27. target.prototype.$classDescriptor = description;
  28. }
  29. }
  30. function propDescriptor(description: string) {
  31. return function (target: any, propName: string) {
  32. // 把所有的属性信息保存到该类的原型中
  33. if(!target.$propDescriptions){
  34. target.$propDescriptions = [];
  35. }
  36. target.$propDescriptions.push({propName,description})
  37. }
  38. }
  39. @classDescriptor('用户')
  40. class User_1 {
  41. @propDescriptor('账号')
  42. loginId: string
  43. @propDescriptor('密码')
  44. loginPwd: string
  45. constructor(loginId: string, loginPwd: string){
  46. this.loginId = loginId
  47. this.loginPwd = loginPwd
  48. }
  49. }
  50. const u = new User_1('234', 'ewr');
  51. printObj(u);

reflect-metadata库

上述代码,美中不足的地方在于,污染了原型链!
而这个库的作用就是:在另外一个地方保存元数据!避免污染原型链
这个库在运行时也需要依赖,所以在生产环境。

Reflect.getMetadata()会得到一个装饰器!
Reflect.metadata()会产生一个装饰器
Reflect.getOwnMetadata()
Reflect.defineMetadata()查官网api吧

  1. import "reflect-metadata"
  2. const key = Symbol.for('descriptor');// 每次都会返回一个独一无二的symbol对象
  3. type construct = new (...args: any[]) => object;
  4. function descriptor1(description: string) {
  5. console.log(description)
  6. // metadata自己可以区分,我们把附加信息加到类还是属性/方法上。但是!写在这里是区分不出来的,得写到类的上一行
  7. return Reflect.metadata(key, description)
  8. }
  9. function printObj(obj: any) {
  10. const cons = Object.getPrototypeOf(obj)
  11. // 输出类的名字
  12. if(Reflect.hasMetadata(key, cons)){
  13. console.log('\n', Reflect.getMetadata(key, cons), Reflect.getOwnMetadata(key, cons));
  14. }else{
  15. console.log('\n', cons.constructor.name, ',', Reflect.getMetadata(key, cons), Reflect.getOwnMetadata(key, cons));
  16. }
  17. // 输出所有的属性描述和属性值
  18. for(const k in obj){
  19. console.log(k)
  20. if(Reflect.hasMetadata(key, obj, k)){
  21. console.log(`\t${Reflect.getMetadata(key, obj, k)}:${obj[k]}`)
  22. }else{
  23. console.log(`\t${k}:${obj[k]}`)
  24. }
  25. }
  26. }
  27. @descriptor1('文章')
  28. class Article {
  29. @descriptor1('方法1')
  30. method(){
  31. console.log('method...')
  32. }
  33. @descriptor1('标题')
  34. title: string
  35. @descriptor1('内容')
  36. content: string
  37. @descriptor1('日期')
  38. date: Date
  39. }
  40. const ar = new Article();
  41. ar.title = 'xxxxx';
  42. ar.content = 'adf234jsd';
  43. ar.date = new Date();
  44. 方法1
  45. 标题
  46. 内容
  47. 日期
  48. 文章
  49. Article , undefined undefined
  50. title
  51. 标题:xxxxx
  52. content
  53. 内容:adf234jsd
  54. date
  55. 日期:Mon Sep 13 2021 12:28:47 GMT+0800 (GMT+08:00)
  56. export default printObj(ar);

深入理解TypeScript#自定义metadataKey

  1. function classDecorator(): ClassDecorator {
  2. return target => {
  3. // 在类上定义元数据,key 为 `classMetaData`,value 为 `a`
  4. Reflect.defineMetadata('classMetaData', 'a', target);
  5. };
  6. }
  7. function methodDecorator(): MethodDecorator {
  8. return (target, key, descriptor) => {
  9. // 在类的原型属性 'someMethod' 上定义元数据,key 为 `methodMetaData`,value 为 `b`
  10. Reflect.defineMetadata('methodMetaData', 'b', target, key);
  11. };
  12. }
  13. @classDecorator()
  14. class SomeClass {
  15. @methodDecorator()
  16. someMethod() {}
  17. }
  18. Reflect.getMetadata('classMetaData', SomeClass); // 'a'
  19. Reflect.getMetadata('methodMetaData', new SomeClass(), 'someMethod'); // 'b'

class-validator和class-transformer库

前者是做校验的,后者是将一个平面对象(通过一对大括号书写的对象,ajax得到的就是)转为一个类的对象

  1. import "reflect-metadata";
  2. import { IsNotEmpty, Max, MaxLength, Min, MinLength, validate } from "class-validator";
  3. class RegUser {
  4. @IsNotEmpty({message: '账号不可为空'})
  5. @MinLength(5, {message: '账号至少5个字符'})
  6. @MaxLength(12, {message: '账号最多12个字符'})
  7. loginId: string
  8. loginPwd: string
  9. @Min(0, {message: '年龄的最小值是0'})
  10. @Max(100, {message: '年龄的最大值是100'})
  11. age: number
  12. gender: '男' | '女'
  13. }
  14. const post = new RegUser()
  15. post.loginId = '777'
  16. post.age = -1
  17. validate(post).then(errors => {
  18. console.log(errors)
  19. })
  1. import "reflect-metadata";
  2. // import { IsNotEmpty, Max, MaxLength, Min, MinLength, validate } from "class-validator";
  3. import { plainToClass, Type } from "class-transformer"
  4. import axios from 'axios'
  5. class User {
  6. id: number
  7. firstName: string
  8. lastName: string
  9. @Type(() => Number)//表示在运行态的时候, age会转换为number类型
  10. age: number
  11. getName(){
  12. return this.firstName + ' ' + this.lastName
  13. }
  14. isAdult(){
  15. return this.age > 36 && this.age < 60
  16. }
  17. }
  18. axios.get('https://api.jsonserve.com/aYbEix').then(resp => resp.data, error => {
  19. console.log(error)
  20. })
  21. .then((users: User[]) => {
  22. const us = plainToClass(User, users)
  23. for(const u of us){
  24. console.log(u.getName(), typeof u.age, u.age)
  25. }
  26. })

装饰器补充

参数装饰器

依赖注入/依赖倒置

要求有三个参数:

  1. 如果方法是静态的,则为类本身;如果方法是实例方法,则为类的原型
  2. 方法名称
  3. 在参数列表中的索引(这是所修饰的第几个方法) ```typescript class MyMath { sum(a: number, @test b: number){
    1. return a+b
    } }

function test(target: any, method: string, index: number){ console.log(target, method, index) } // MyMath {} sum 1

  1. <a name="G3wwO"></a>
  2. ## TS自动注入的元数据
  3. **如果安装了 refelct-metadata,并且导入了该库,并在某个成员上添加了元数据,并且启用了配置emitDecoratorMetadata,则TS在编译结果中会将约束的类型作为元数据加入到相应位置**
  4. **这样一来,TS的类型检查(约束)将有机会在运行时进行!**
  5. ```typescript
  6. class User {
  7. @Reflect.metadata("a", '账号id')
  8. loginId: string
  9. @Reflect.metadata("a", '用户年龄')
  10. age: number
  11. }
  12. const u = new User()
  13. console.log(Reflect.getMetadata('a', u, 'loginId'))//账号id
  14. console.log(Reflect.getMetadata('design:type', u, 'loginId'))//[Function: String]
  15. //编译结果如下:
  16. class User {
  17. }
  18. __decorate([
  19. Reflect.metadata("a", '账号id'),
  20. __metadata("design:type", String)
  21. ], User.prototype, "loginId", void 0);
  22. __decorate([
  23. Reflect.metadata("a", '用户年龄'),
  24. __metadata("design:type", Number)
  25. ], User.prototype, "age", void 0);
  26. const u = new User();
  27. console.log(Reflect.getMetadata('a', u, 'loginId'));

面向对象设计模式之AOP

AOP: aspect oriented programming

将一些在业务中共同出现的功能块,横向切分,以达到分离关注点的目的

比如:要提交数据,但是此前要数据要先通过校验,所以应该把校验功能抽离出去