概述
装饰器(decorator)是面向对象的概念(在java中叫注解,c#中叫特征)
angular大量使用,react中也会用到,之前前端用的很少,目前越来越多
JS支持装饰器,但是目前不是最终阶段,还未成为标准
在TS中使用装饰器,需要开启 experimentalDecorators 配置
解决的问题
装饰器能够带来额外的信息量,可以达到分离关注点的目的。
比如有个User类,它里面的很多属性,需要进行规则校验,怎么做合适呢
- 把所有校验逻辑抽离到一个单独的函数内,即便让其成为类的成员方法,也有缺陷
 
- 我们是写属性的时候最清楚这个属性想表达什么,校验规则是啥;而不是在函数里一一处理
 - 虽然对于重复代码可以一直抽离为新的函数,但是这样代码仍然多,函数很多
 
关注点的问题:
- 在定义某个东西时,应该最清楚该东西的情况,而不是在其他时刻再去根据我们自己的判断写一次代码
- 简述为:信息书写位置的问题
 
 - 重复代码/过多函数问题
 
上述两个问题产生的根源:某些信息,在定义时,能够附加的信息量有限。导致虽然我们清楚这些附加信息(比如我们写的注释的信息),但是编程语言、计算机它不知道啊!
装饰器的作用:为某些属性、类、参数、方法提供元数据(metadata)信息
元数据:描述数据 的 数据/信息
装饰器的本质
在JS中,装饰器本质上是一个函数。(装饰器是要参与运行的!)
装饰器可以修饰:类/成员(属性+方法)/参数
类装饰器
类装饰器的本质
记住!它必须是一个函数,该函数必须接收一个参数,该必须参数表示类本身(构造函数本身)
使用装饰器: @函数名
在TS中如何约束一个变量为类?
- target: Function
- 但是不能new target了,因为TS不知道你到底是类还是普通函数
 
 target: new (…args: any[]) => object
void:仅运行函数
- 返回一个新的类:会将新的类替换掉装饰目标,所以这里要小心啊,一般是用于加强功能,取继承即可
 
function d1(name: string){ console.log(‘这个d1是装饰器的包装层, 普通函数’) return function (target: constructor){ console.log(‘这个函数才是真正的装饰器’) } }
@d1(‘wjw’)// 这里这个d1(‘wjw’)只是个普通的函数 class A { prop1: string }
<a name="vHLtA"></a>## 多个类装饰器函数运行顺序**真正返回的类装饰器函数是从下至上运行,但是包装层即普通函数是正常的从上至下顺序**<br />**如果还有方法和属性装饰器的包装层,则先执行他们,最后再执行类装饰器的包装层!**```typescripttype constructor = new (...args: any[]) => objectfunction d1(name: string){console.log('1. 这个d1是装饰器的包装层')return function (target: constructor){console.log('4. 这个函数才是真正的d1装饰器')}}function d2(name: string){console.log('2. 这个d2是装饰器的包装层')return function (target: constructor){console.log('3. 这个函数才是真正的d2装饰器')}}@d1('wjw')@d2('wqq')class A {prop1: stringconstructor(){this.prop1 = 'wjw'console.log('A')}}
编译后的结果:
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);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;return c > 3 && r && Object.defineProperty(target, key, r), r;};function d1(name) {console.log('1. 这个d1是装饰器的包装层');return function (target) {console.log('4. 这个函数才是真正的d1装饰器');};}function d2(name) {console.log('2. 这个d2是装饰器的包装层');return function (target) {console.log('3. 这个函数才是真正的d2装饰器');};}let A = class A {constructor() {this.prop1 = 'wjw';console.log('A');}};A = __decorate([d1('wjw'),d2('wqq')], A);
成员装饰器
属性装饰器
属性装饰器也是一个函数,该函数需要两个参数
- 如果是静态属性,则为类本身。如果是实例属性,则为类的原型
 - 
方法装饰器
同样,方法装饰器也是一个函数,该函数需要三个参数
 同上
- 固定为一个字符串,表示方法名
 - 属性描述对象,也是Object.defineProperty的第三个参数
 
练习:类和属性的描述装饰器
function printObj(obj: any) {if(obj.$classDescriptor){console.log(obj.$classDescriptor);}else{console.log(obj.__proto__.constructor.name);}if(obj.$propDescriptions){console.log(obj.$propDescriptions);}else{obj.$propDescriptions = [];}for(const key in obj){if(obj.hasOwnProperty(key)){const prop = obj.$propDescriptions.find((p: any) => p.propName === key)if(prop){console.log(`\t${prop.description}: ${obj[key]}`)}else{console.log(`\t${key}: ${obj[key]}`)}}}}type construct = new (...args: any[]) => object;function classDescriptor(description: string) {return function (target: Function) {// 保存到该类到原型中target.prototype.$classDescriptor = description;}}function propDescriptor(description: string) {return function (target: any, propName: string) {// 把所有的属性信息保存到该类的原型中if(!target.$propDescriptions){target.$propDescriptions = [];}target.$propDescriptions.push({propName,description})}}@classDescriptor('用户')class User_1 {@propDescriptor('账号')loginId: string@propDescriptor('密码')loginPwd: stringconstructor(loginId: string, loginPwd: string){this.loginId = loginIdthis.loginPwd = loginPwd}}const u = new User_1('234', 'ewr');printObj(u);
reflect-metadata库
上述代码,美中不足的地方在于,污染了原型链!
而这个库的作用就是:在另外一个地方保存元数据!避免污染原型链
这个库在运行时也需要依赖,所以在生产环境。
Reflect.getMetadata()会得到一个装饰器!
Reflect.metadata()会产生一个装饰器
Reflect.getOwnMetadata()
Reflect.defineMetadata()查官网api吧
import "reflect-metadata"const key = Symbol.for('descriptor');// 每次都会返回一个独一无二的symbol对象type construct = new (...args: any[]) => object;function descriptor1(description: string) {console.log(description)// metadata自己可以区分,我们把附加信息加到类还是属性/方法上。但是!写在这里是区分不出来的,得写到类的上一行return Reflect.metadata(key, description)}function printObj(obj: any) {const cons = Object.getPrototypeOf(obj)// 输出类的名字if(Reflect.hasMetadata(key, cons)){console.log('\n', Reflect.getMetadata(key, cons), Reflect.getOwnMetadata(key, cons));}else{console.log('\n', cons.constructor.name, ',', Reflect.getMetadata(key, cons), Reflect.getOwnMetadata(key, cons));}// 输出所有的属性描述和属性值for(const k in obj){console.log(k)if(Reflect.hasMetadata(key, obj, k)){console.log(`\t${Reflect.getMetadata(key, obj, k)}:${obj[k]}`)}else{console.log(`\t${k}:${obj[k]}`)}}}@descriptor1('文章')class Article {@descriptor1('方法1')method(){console.log('method...')}@descriptor1('标题')title: string@descriptor1('内容')content: string@descriptor1('日期')date: Date}const ar = new Article();ar.title = 'xxxxx';ar.content = 'adf234jsd';ar.date = new Date();方法1标题内容日期文章Article , undefined undefinedtitle标题:xxxxxcontent内容:adf234jsddate日期:Mon Sep 13 2021 12:28:47 GMT+0800 (GMT+08:00)export default printObj(ar);
function classDecorator(): ClassDecorator {return target => {// 在类上定义元数据,key 为 `classMetaData`,value 为 `a`Reflect.defineMetadata('classMetaData', 'a', target);};}function methodDecorator(): MethodDecorator {return (target, key, descriptor) => {// 在类的原型属性 'someMethod' 上定义元数据,key 为 `methodMetaData`,value 为 `b`Reflect.defineMetadata('methodMetaData', 'b', target, key);};}@classDecorator()class SomeClass {@methodDecorator()someMethod() {}}Reflect.getMetadata('classMetaData', SomeClass); // 'a'Reflect.getMetadata('methodMetaData', new SomeClass(), 'someMethod'); // 'b'
class-validator和class-transformer库
前者是做校验的,后者是将一个平面对象(通过一对大括号书写的对象,ajax得到的就是)转为一个类的对象
import "reflect-metadata";import { IsNotEmpty, Max, MaxLength, Min, MinLength, validate } from "class-validator";class RegUser {@IsNotEmpty({message: '账号不可为空'})@MinLength(5, {message: '账号至少5个字符'})@MaxLength(12, {message: '账号最多12个字符'})loginId: stringloginPwd: string@Min(0, {message: '年龄的最小值是0'})@Max(100, {message: '年龄的最大值是100'})age: numbergender: '男' | '女'}const post = new RegUser()post.loginId = '777'post.age = -1validate(post).then(errors => {console.log(errors)})
import "reflect-metadata";// import { IsNotEmpty, Max, MaxLength, Min, MinLength, validate } from "class-validator";import { plainToClass, Type } from "class-transformer"import axios from 'axios'class User {id: numberfirstName: stringlastName: string@Type(() => Number)//表示在运行态的时候, age会转换为number类型age: numbergetName(){return this.firstName + ' ' + this.lastName}isAdult(){return this.age > 36 && this.age < 60}}axios.get('https://api.jsonserve.com/aYbEix').then(resp => resp.data, error => {console.log(error)}).then((users: User[]) => {const us = plainToClass(User, users)for(const u of us){console.log(u.getName(), typeof u.age, u.age)}})
装饰器补充
参数装饰器
依赖注入/依赖倒置
要求有三个参数:
- 如果方法是静态的,则为类本身;如果方法是实例方法,则为类的原型
 - 方法名称
 - 在参数列表中的索引(这是所修饰的第几个方法)
```typescript
class MyMath {
 sum(a: number, @test b: number){
} }return a+b
 
function test(target: any, method: string, index: number){ console.log(target, method, index) } // MyMath {} sum 1
<a name="G3wwO"></a>## TS自动注入的元数据**如果安装了 refelct-metadata,并且导入了该库,并在某个成员上添加了元数据,并且启用了配置emitDecoratorMetadata,则TS在编译结果中会将约束的类型作为元数据加入到相应位置****这样一来,TS的类型检查(约束)将有机会在运行时进行!**```typescriptclass User {@Reflect.metadata("a", '账号id')loginId: string@Reflect.metadata("a", '用户年龄')age: number}const u = new User()console.log(Reflect.getMetadata('a', u, 'loginId'))//账号idconsole.log(Reflect.getMetadata('design:type', u, 'loginId'))//[Function: String]//编译结果如下:class User {}__decorate([Reflect.metadata("a", '账号id'),__metadata("design:type", String)], User.prototype, "loginId", void 0);__decorate([Reflect.metadata("a", '用户年龄'),__metadata("design:type", Number)], User.prototype, "age", void 0);const u = new User();console.log(Reflect.getMetadata('a', u, 'loginId'));
面向对象设计模式之AOP
AOP: aspect oriented programming
将一些在业务中共同出现的功能块,横向切分,以达到分离关注点的目的
比如:要提交数据,但是此前要数据要先通过校验,所以应该把校验功能抽离出去
