一、装饰器模式
在讲装饰器之前,先了解一下什么是装饰器模式吧!
1. 概要
开发过程中,如果希望动态给某个类添加一些属性或者方法,但是你又不希望这个类派生的对象受到影响,那么装饰器模式就可以给你带来这样的体验。它的定义就是“在不改变原对象的基础上,通过对其进行包装拓展,使得原有对象可以动态具有更多功能,从而满足用户的更复杂需求”。
举个例子,一部手机,你可以买各种花里胡哨的手机壳等,这些手机壳其实就起到了装饰的作用,对手机没有任何影响。
那么装饰器模式的特点就来了:
- 不影响原有功能;
- 可装饰多个;
可以看到,装饰器模式就如同上图,将目标层层包裹,给目标添加层层装饰。
class Triangle {
draw() {
console.log('画了个三角.')
}
}
class Decorator {
constructor(triangle) {
this.triangle = triangle;
}
draw() {
this.triangle.draw();
this.setBackgroundColor();
}
setBackgroundColor() {
console.log('设置了背景色.')
}
}
let triangle = new Triangle();
let decorator = new Decorator(triangle);
decorator.draw();
二、AOP
看了装饰器模式之后,是不是觉得又多了解了一个设计模式,可以在实际开发中使用了。AOP也得了解下。
1. 概要
OOP大家肯定再熟悉不过了,面向对面编程,封装、集成、多态是它的思想,相信也在实际开发中运用自如了。而AOP是一种新的编程方式,面向切面编程。OOP是把系统看成多个对象进行交互,而AOP是把系统分为不同的关注点,或者称其为“切面”(Aspect)。
AOP主要把业务逻辑无关的功能进行抽离,这些与业务逻辑无关的内容如日志打印、数据统计、异常处理、权限控制等,再通过动态注入的方式注入到业务代码中。这样做既保证了业务内部高内聚,模块之间低耦合,方便管理与业务无关的模块。
2. JS中AOP相关实现
这里讲AOP其实是希望其于装饰器模式放在一起看,两者最终解决的问题都是相同的。大家看下面几个代码片段。
// 后置通知
Function.prototype.before = function(afterFn) {
const _this = this;
return function() {
const ret = _this.apply(this, arguments);
afterFn.apply(this, arguments);
return ret;
}
}
// 假如你要在window.onload后写一些别的内容,可以这么做:
window.onload = window.onload.after(function(){
/***这是你的代码***/
});
// 前置通知
Function.prototype.before = function(beforeFn) {
const _this = this;
return function() {
beforeFn.apply(this, arguments);
return _this.apply(this, arguments);
}
}
// 假如你想看一段复杂的函数执行时间,可以这么做
function abcd(){
// ...这是很多很多的循环代码
}
// 时间差
let t1,t2;
abcd = abcd.before(function(){
t1=+new Date();//方法执行前统计时间
})._after(function(){
t2=+new Date();//方法执行后统计时间
let dis_time=t2-t1;//这是时间差
});
// 环绕通知
Function.prototype.around = function(func) {
function JoinPoint(obj, args) {
let isApply = false; // 判断是否执行过目标函数
let result = null; // 保存目标函数的执行结果
this.source = obj; // 目标函数对象
this.args = args; // 目标函数对象传入的参数
/**
* 目标函数的代理执行函数
* 如果被调用过,不能重复调用
* 目标函数的返回结果
*/
this.invoke = function(thiz) {
if(isApply) {
return;
}
isApply = true;
result = this.source.apply(thiz || this.source, this.args);
return result;
};
// 获取目标函数执行结果
this.getResult = function() {
return result;
}
}
const __self = this;
return function() {
const args = [new JoinPoint(__self, arguments)];
return func.apply(this, args);
}
}
// 假如你要做表单验证
function validate(){
if(valid){
return false;
}
return true;
}
function submit(){
}
submit = submit._around(function(fn){
if(validate()){
fn.invoke(this);
}else{
}
});
三、什么是装饰器
了解了装饰器模式和AOP,虽然能解决我们的问题,但是写法不爽,在JAVA中有注解,前端有没有注解呢?进入正题,装饰器语法糖来袭!
1. 概要
记得那是遥远的2015年,有位牛人Yehuda Katz提出了装饰器的概念,从那时开始ES6、Typescript、Angular等纷纷拥抱,但是这只是个尚未明确的提案。不过哪天提案定案之后,需要重写。
言归正传,装饰器(Decorator)是一种与类(class)相关的预发,用来注释或修改类和类的方法。
装饰器听起来很高端,其实它就是一种函数。
2. 为什么要用它?
装饰器提案的出现其实很方便的解决了一些类和属性装饰器的支持,而且语法简洁,在解耦方面更上一层楼。
3. 装饰器如何使用?
@ + 函数名
它可以写在类或者类的方法上面。
例如:
function testAble(target) {
target.isTestable = true;
}
@testAble
class MyTestClass {}
console.log(MyTestClass.isTestable); // true
说到这里会有个概念,AOP,面向切面编程。大家熟知的都是OOP,面向对象编程,主要思想就是封装、继承和多态,这里就不过多冗述OOP了。
切入正题,来到AOP,其实就是把与业务逻辑无关的功能进行抽离,例如权限控制、异常处理、日志打印等等无关功能,不需要在代码中体现。
四、JS装饰器
1. 类的装饰
装饰器可以用来装饰整个类。
拿上面的代码举例
function testAble(target) {
target.isTestable = true;
}
@testAble
class MyTestClass {}
MyTestClass.isTestable; // true
上面代码中,@testAble就是一个装饰器,它修改了MyTestClass这个类,为其添加了静态属性isTestable属性,testAble函数的参数target就是MyTestClass类本身。
所以:装饰器是一个对类进行处理的函数。装饰器函数的第一个参数就是所要装饰的目标类。
如果觉得一个参数不够用,可以在装饰器外面再封装一层函数。
function testAble(isTestable) {
return function(target) {
target.isTestable = isTestable;
}
}
@testAble(true)
class MyTestClass {}
MyTestClass.isTestable; // true
@testAble(false)
class MyTestClass1 {}
MyTestClass1.isTestable; // false
上面代码中,装饰器testAble可以接受参数,这就等于可以修改装饰器的行为。
注意,装饰器对类的行为的改变,是代码编译时发生的,而不是在运行时。这意味着,装饰器能在编译阶段运行代码。也就是说,装饰器本质就是编译时执行的函数。
如果想通过装饰器为类添加实例属性,可以通过目标类的prototype对象操作。
function testAble(target) {
target.prototype.isTestable = true;
}
@testAble
class MyClass {}
let myClass = new MyClass();
myClass.isTestable // true
2. 方法的装饰
装饰器不仅可以装饰类,同样可以装饰类的方法,但是不同的是,如果装饰类的方法,装饰器函数的入参就变成了三个target、name、descriptor。
- target:类的原型对象
- name:装饰的属性名
- descriptor:该属性的描述对象
class Person {
@readonly
name() { return `${this.name}` }
}
function readonly(target, name, descriptor){
// descriptor对象原来的值如下
// {
// value: specifiedFunction,
// enumerable: false,
// configurable: true,
// writable: true
// };
descriptor.writable = false;
return descriptor;
}
readonly(Person.prototype, 'name', descriptor);
// 类似于
Object.defineProperty(Person.prototype, 'name', descriptor);
如果同一个方法有多个装饰器,从外到内进入,从内向外执行。
function dec(id){
console.log('evaluated', id);
return (target, property, descriptor) => console.log('executed', id);
}
class Example {
@dec(1)
@dec(2)
method(){}
}
// evaluated 1
// evaluated 2
// executed 2
// executed 1
3. 注意事项
JS装饰器,只能修饰类或者类的方法,存在变量提升的问题。
如果一定要装饰函数,可以采用高阶函数的形式直接执行。
五、TS装饰器
讲完了JS装饰器,再来看看TS装饰器。
1. 概要
JS装饰器,可以修饰类和类的方法。而TS装饰器,进行了一些拓展。写法相同,但是它可以作用于
- 类的声明
- 方法
- 访问器
- 属性
- 方法参数
2. TS如何开启装饰器特性
tsconfig.json中开启experimentalDecorators
{
"compilerOptions": {
"target": "ES5",
"experimentalDecorators": true
}
}
3. 装饰器分类
// 类装饰器
@classDecorator
class MyClass {
// 属性装饰器
@prooertyDecorator
name: string;
// 方法装饰器
@methodDecorator
setName(
// 参数装饰器
@parameterDecorator firstName: string
) {};
// 访问器装饰器
@accessDecorator
get age() {}
}
4. 类装饰器
类装饰器在类声明之前被声明(紧靠着类声明)。 类装饰器应用于类构造函数,可以用来监视,修改或替换类定义。 类装饰器不能用在声明文件中( .d.ts),也不能用在任何外部上下文中(比如declare的类)。
类装饰器表达式会在运行时当作函数被调用,类的构造函数作为其唯一的参数。
如果类装饰器返回一个值,它会使用提供的构造函数来替换类的声明。
其声明如下:
type ClassDecorator = <TFunction extends Function>(target: TFunction) => TFunction | void;
-
参数:
- target:类的构造函数
-
返回值:如果累装饰器返回了一个非空的值,那么该值将用来替代原本的类
function decorateClass<T>(constructor: T) {
console.log(constructor === A) // true
}
@decorateClass
class A {
constructor() {
}
}
5. 方法装饰器
方法装饰器声明在一个方法的声明之前(紧靠着方法声明)。 它会被应用到方法的 属性描述符上,可以用来监视,修改或者替换方法定义。 方法装饰器不能用在声明文件( .d.ts),重载或者任何外部上下文(比如declare的类)中。
其声明如下:
declare type MethodDecorator = <T>(target: Object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<T>) => TypedPropertyDescriptor<T> | void;
-
参数
- target:修饰静态方法时,是类的构造方法;否则是类的原型(prototype)
- propertyKey: 方法名
- descriptor:方法的描述对象
-
返回值:如果方法装饰器返回了一个非空的值,那么该值将用来替代方法原本的描述对象
function decorateMethod(target: any,key: string,descriptor: PropertyDescriptor){
console.log('target === A',target === A) // 是否类的构造函数
console.log('target === A.prototype',target === A.prototype) // 是否类的原型对象
console.log('key',key) // 方法名
console.log('descriptor',descriptor) // 成员的属性描述符 Object.getOwnPropertyDescriptor
}
class A {
@decorateMethod // 输出 true false 'staticMethod' Object.getOwnPropertyDescriptor(A,'sayStatic')
static staticMethod(){}
@decorateMethod // 输出 false true 'instanceMethod' Object.getOwnPropertyDescriptor(A.prototype,'sayInstance')
instanceMethod(){
}
}
6. 访问器装饰器
访问器装饰器与方法装饰器类似,其参数如下:
-
参数
- target:当其装饰静态成员时为类的构造函数,装饰实例成员时为类的原型对象
- propertyKey: 被装饰的成员名
- descriptor:成员的属性描述符(Object.getOwnPropertyDecriptor(target, key))
Typescript不允许同时在一个属性的getter和setter同时设置装饰器
import moment from 'moment';
function formatterTime(target: any, key: string, descriptor: PropertyDescriptor) {
const set = descriptor.set;
descriptor.set = function(time) {
set.call(this, moment(time).format('YYYY-MM-DD'))
}
}
class ConfigClass {
private time: Date = new Date();
@formatterTime
set Time(time: Date) {
this.time = time;
}
}
const config = new ConfigClass();
config.Time = new Date();
console.log(config.Time); // 按照YYYY-MM-DD格式输出
7. 属性装饰器
属性需要等到类被实例化后才能拿到具体的结果,因此多用于收集信息。其类型声明如下:
type PropertyDecorator = (target: Object, propertyKey: string | symbol) => void;
-
参数
- target:修饰静态方法时,是类的构造方法;否则是类的原型(prototype)
- propertyKey: 方法名
-
返回值:忽略返回结果
8. 参数装饰器
参数装饰器的表达式将在运行时作为函数调用
-
参数:
- target: 当其装饰静态成员时为类的构造函数,装饰实例成员时为类的原型对象
- key: 参数名。
- index: 参数在函数参数列表中的索引
function required(target: any, key: string, index: number) {
console.log(target === A)
console.log(target === A.prototype)
console.log(key)
console.log(index)
}
class A {
saveData(@required name: string){} // 输出 false true name 0
}
9. 装饰器执行相关知识
装饰器只在执行时应用一次。
function fn() {
console.log('Function fn is apply.')
}
@fn
class MyClass {}
// Function fn is apply.
哪怕我们没有使用MyClass这个类,终端也会打印。
所以在TS中,装饰器的执行顺序为:
属性装饰器 -> 方法装饰器 -> 参数装饰器 -> 类装饰器
如果同一个类型的装饰器有多个,总是先执行后面的装饰器。
四、实战
通过装饰器实现对方法的参数校验的功能,通过参数装饰器为参数打标,使用方法装饰器装饰原有方法,运行前对类型检查。
type Validator = (value: unknown) => boolean | void;
// 收集不同方法的参数校验器
const validatorMap = new Map<string, any>();
export function applyValidator(validator: Validator, description?: string) {
return function (target: any, key: string, idx: number) {
let validators: {};
// 获取已存在校验器,没有则加入到map
if (validatorMap.has(key)) {
validators = validatorMap.get(key);
} else {
validators = {};
validatorMap.set(key, validators);
}
if (!Array.isArray(validators[idx])) {
validators[idx] = [];
}
validators[idx].push({
rule: validator,
description,
});
};
}
export function validate(target: any, keyName: string, descriptor: PropertyDescriptor) {
const origin = descriptor.value;
descriptor.value = function (...args: unknown[]) {
// 方法不需要校验,直接运行
if (!validatorMap.has(keyName)) {
return origin.apply(this, args);
}
const validatorObj = validatorMap.get(keyName);
Object.keys(validatorObj).forEach(key => {
const validators = validatorObj[key];
if (!validate) {
return;
}
if (args.length > 0) {
for (let i = 0; i < validators.length; i++) {
const validator = validators[i].rule;
const description = validators[i].description;
if (!validator(args[key])) {
throw new TypeError(`【参数位置:${+key + 1}】:${description}`);
}
}
} else {
throw new TypeError('请填写必要参数');
}
});
return origin.apply(this, args);
};
}
function getType(param: any) {
return Object.prototype.toString.call(param).replace(/[object (.*)]/, '$1').toLowerCase();
}
export const isString = (description?: any) => applyValidator(x => getType(x) === 'string', description || '参数类型为String');
@validate
static async workspaceDetail(@isString() workspaceId: string) {
return await fetchWorkspaceDetail(workspaceId);
}
class Test {
getDetail(@isString() id: string) {
console.log(id);
}
new Test().getDetail(123); // 【参数位置:1:参数类型为String】
new Test().getDetail('123'); // '123'