概述
装饰器(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 />**如果还有方法和属性装饰器的包装层,则先执行他们,最后再执行类装饰器的包装层!**
```typescript
type constructor = new (...args: any[]) => object
function 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: string
constructor(){
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: string
constructor(loginId: string, loginPwd: string){
this.loginId = loginId
this.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 undefined
title
标题:xxxxx
content
内容:adf234jsd
date
日期: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: string
loginPwd: string
@Min(0, {message: '年龄的最小值是0'})
@Max(100, {message: '年龄的最大值是100'})
age: number
gender: '男' | '女'
}
const post = new RegUser()
post.loginId = '777'
post.age = -1
validate(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: number
firstName: string
lastName: string
@Type(() => Number)//表示在运行态的时候, age会转换为number类型
age: number
getName(){
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的类型检查(约束)将有机会在运行时进行!**
```typescript
class User {
@Reflect.metadata("a", '账号id')
loginId: string
@Reflect.metadata("a", '用户年龄')
age: number
}
const u = new User()
console.log(Reflect.getMetadata('a', u, 'loginId'))//账号id
console.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
将一些在业务中共同出现的功能块,横向切分,以达到分离关注点的目的
比如:要提交数据,但是此前要数据要先通过校验,所以应该把校验功能抽离出去