装饰器的那些事儿

134 阅读6分钟

一、装饰器模式

在讲装饰器之前,先了解一下什么是装饰器模式吧!

1. 概要

开发过程中,如果希望动态给某个类添加一些属性或者方法,但是你又不希望这个类派生的对象受到影响,那么装饰器模式就可以给你带来这样的体验。它的定义就是“在不改变原对象的基础上,通过对其进行包装拓展,使得原有对象可以动态具有更多功能,从而满足用户的更复杂需求”。

举个例子,一部手机,你可以买各种花里胡哨的手机壳等,这些手机壳其实就起到了装饰的作用,对手机没有任何影响。

那么装饰器模式的特点就来了:

  1. 不影响原有功能;
  2. 可装饰多个;

image.png

可以看到,装饰器模式就如同上图,将目标层层包裹,给目标添加层层装饰。


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(thisarguments);
        afterFn.apply(thisarguments);
        return ret;
    }
}

// 假如你要在window.onload后写一些别的内容,可以这么做:
window.onload = window.onload.after(function(){
    /***这是你的代码***/
});
// 前置通知
Function.prototype.before = function(beforeFn) {
    const _this = this;
    return function() {
        beforeFn.apply(thisarguments);
        return _this.apply(thisarguments);
    }
}

// 假如你想看一段复杂的函数执行时间,可以这么做
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.sourcethis.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装饰器,进行了一些拓展。写法相同,但是它可以作用于

  1. 类的声明
  2. 方法
  3. 访问器
  4. 属性
  5. 方法参数

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;
  • 参数:

    1. 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;
  • 参数

    1. target:修饰静态方法时,是类的构造方法;否则是类的原型(prototype)
    2. propertyKey: 方法名
    3. 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. 访问器装饰器

访问器装饰器与方法装饰器类似,其参数如下:

  • 参数

    1. target:当其装饰静态成员时为类的构造函数,装饰实例成员时为类的原型对象
    2. propertyKey: 被装饰的成员名
    3. 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(thismoment(time).format('YYYY-MM-DD'))
    }
}

class ConfigClass {
    private timeDate = 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;
  • 参数

    1. target:修饰静态方法时,是类的构造方法;否则是类的原型(prototype)
    2. propertyKey: 方法名
  • 返回值:忽略返回结果

8. 参数装饰器

参数装饰器的表达式将在运行时作为函数调用

  • 参数:

    1. target: 当其装饰静态成员时为类的构造函数,装饰实例成员时为类的原型对象
    2. key: 参数名。
    3. 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<stringany>();

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'