装饰器模式在前端的应用

2,413 阅读4分钟

装饰器模式

   装饰器模式(Decorator Pattern)允许向一个现有的对象添加新的功能,同时又不改变其结构。这种类型的设计模式属于结构型模式,它是作为现有的类的一个包装。

装饰器就是为了解决在不修改原本组件、接口或者类的时候为其添加额外的功能。从本质上看装饰器模式是一个包装模式((Wrapper Pattern),它是通过封装其他对象达到设计的目的。

为一个对象创建新的功能时,当然,我们可以用继承来实现。

class Shape {
  constructor(name) {
    this.name = name
  }

  draw() {
    console.log(`draw ${this.name}`)
  }
}

class ShapeColor extends Shape {
  constructor(name) {
    super(name)
  }

  setColor(color) {
    console.log(`color the ${this.name} ${color}`)
  }
}

const circle = new ShapeColor('triangle')
circle.setColor('blue')
circle.draw()

但是这样的方式会存在父类Shape和子类ShapeColor之间存在强耦合性的问题,一旦父类发生改变,那么子类也会随之发生改变,因此这种方式并不能灵活。

为了解决这些问题,我们需要装饰器模式,装饰器模式属于结构型的设计模式,其的特点是:

  • 在不改变对象的基础上;
  • 在程序运行期间给对象动态的添加职责;
class Shape {
  constructor(name) {
    this.name = name
  }

  draw() {
    console.log(`draw ${this.name}`)
  }
}

class ColorDecorator {
  constructor(shape) {
    this.shape = shape
  }

  draw() {
    this.setColor()
    this.shape.draw()
  }

  setColor() {
    console.log(`color the ${this.shape.name}`)
  }
}

let circle = new Shape('circle');
circle.draw();

let decorator = new ColorDecorator(circle);
decorator.draw();

通过ColorDecorator对象实现了一个相同的draw()方法,并在其中封装了setColor()这个额外的职责方法,用户同样在调用draw()方法时,也调用了上一个对象Shape的draw()方法。我们可以以此类推,给对象添加上色,设置边框的一系列职责。

实际上,装饰器也是一种包装器,把上个对象包装到某个对象中,层层包装。

SE7中的装饰器

ES7版本中引入了装饰器。

@addSkill
class Person { }

function addSkill(target) {
    target.say = "hello world"; //直接添加到类中
    target.prototype.eat = "apple"; //添加到类的原型对象中
}
var personOne = new Person();

console.log(Person['say']); // 'hello world'
console.log(personOne['eat']); // 'apple'

如果在class上添加一个装饰器,发现在浏览器中是无法直接运行的,如果要想使用它,还需要提前做一些准备工作,对它进行编译。

准备过程: 需要使用插件来让浏览器支持Decorator。

  1. 新建一个文件夹,然后安装插件:
npm i babel-plugin-transform-decorators-legacy babel-register --save-dev
  1. 创建文件 complie.js
const babel = require('babel-register');

babel({
    plugins: ['transform-decorators-legacy']
});

require('./app.js');

3.创建app.js

@addSkill
class Person { }

function addSkill(target) {
    target.say = "hello world"; //直接添加到类中
    target.prototype.eat = "apple"; //添加到类的原型对象中
}
var personOne = new Person();

console.log(Person['say']); // 'hello world'
console.log(personOne['eat']); // 'apple'
  1. 执行compile.js 当然也可以用ts来写,效果是一样。

类装饰器

类修饰器是一个对类进行处理的函数,他的第一个参数target是装饰器要处理的目标类。

不带参数的类装饰器

@addSkill
class Person { }

function addSkill(target) {
    target.prototype.eat = "apple";
}
var personOne = new Person();

console.log(personOne.eat);

带参数的装饰器

@addSkill('I love')
class Person { }

function addSkill(msg) {
    return function(target){
        target.prototype.eat = msg + ' ' + "apple";
    }
}
var personOne = new Person();

console.log(personOne.eat);

问题: 装饰器在什么时候执行? 装饰器对类的行为的改变,是代码编译时发生的,而不是在运行时。这意味着,装饰器能在编译阶段运行代码。也就是说,装饰器本质就是编译时执行的函数。

方法装饰器

装饰器还可以用在类的某个方法上,成为方法装饰器。

class Person { 
    constructor(name){
        this.name = name;
    }

    @MyName
    getName(){
        console.log(this.name);
        return this.name;
    }
}

function MyName(target, key, descriptor){
    // console.log(target, key, descriptor);
    const fn = descriptor.value;

    descriptor.value = function(...args){
        console.log(`${key} is called...` );

        // fn && fn(...args);
        fn && fn.apply(this, args);
    }
}

let person = new Person('Tom');
person.getName();

参数说明

target: 类的原型对象,上例是Person.prototype
key: 所要修饰的属性名  name
descriptor: 该属性的描述对象

@装饰器只能用于类和类的方法。 装饰器第一个参数是类的原型对象,上例是Person.prototype,装饰器的本意是要“装饰”类的实例,但是这个时候实例还没生成,所以只能去装饰原型(这不同于类的装饰,那种情况时target参数指的是类本身);第二个参数是所要装饰的属性名,第三个参数是该属性的描述对象。

多装饰器

如果同一个方法有多个装饰器,会像剥洋葱一样,先从外到内进入,然后由内向外执行。

class Person { 
    constructor(name){
        this.name = name;
    }

    @myName(1)
    @myName(2)
    getName(){
        console.log(this.name);
        return this.name;
    }
}

function myName(msg) {
    console.log('装饰器: ' + msg);
    
    return (target, key, descriptor) => {
        console.log('装饰器执行: ' + msg);
    }
}

let person = new Person('Tom');
person.getName();

遵循洋葱圈模型,从外向里-进入,然后从内向外-执行。

结果

装饰器: 1
装饰器: 2
装饰器执行: 2
装饰器执行: 1
Tom

装饰器不能作用于函数

装饰器只能用于类和类的方法,不能用于函数,因为存在函数提升。

var counter = 0;

var add = function () {
  counter++;
};

@add
function foo() {
}

实际执行相当于:

var counter;
var add;

@add
function foo() {
}

counter = 0;

add = function () {
  counter++;
};

如果一定要装饰函数的话,可以采用高阶函数的形式来直接执行。

function doSomething(name) {
  console.log('Hello, ' + name);
}

function loggingDecorator(wrapped) {
  return function() {
    console.log('Starting');
    const result = wrapped.apply(this, arguments);
    console.log('Finished');
    return result;
  }
}

const wrapped = loggingDecorator(doSomething);

Angular中的装饰器

Angular中的装饰器,功能非常强大,有系统级的Component,Module,Input等装饰器,还可以自定义装饰器 class decorator, method decorator, property decorator。而且自定义的装饰器,不只可以装饰类和类方法,可以装饰类的属性。

export function CLog(): ClassDecorator {
    // tslint:disable-next-line:only-arrow-functions
    return function(constructor: any) {
        const visit = constructor.prototype.visit;

        constructor.prototype.visit = function(...args) {
            console.log(this.title + ' visit...');

            visit.apply(this, ...args);
        };
    };
}
export function MLog(): MethodDecorator {
    return (instance, propertyKey: string, descriptor: any) => {
        console.log(instance, descriptor);

        const fn = descriptor.value;

        descriptor.value = (...args) => {
            console.log(`${propertyKey} is called.`);

            fn.apply(instance, ...args);
        };
    };
}
export function VLog(instance, propertyKey) {
    let value = instance[propertyKey];

    Object.defineProperty(instance, propertyKey, {
        get(){
            return value;
        },
        set(newVal){
            if (newVal > 5) {
                throw new Error('cannot be greater than 6.');
            }
            else {
                value = newVal;
            }
        }
    });
}
import { Component, OnInit } from '@angular/core';
import { CLog } from './decorators/clog';
import { MLog } from './decorators/mlog';
import { VLog } from './decorators/vlog';

// https://www.cnblogs.com/oxspirt/p/14362644.html

@Component({
  selector: 'app-dec-comp',
  templateUrl: './decorator-comp.component.html'
})
@CLog()
export class DecoratorComponent implements OnInit {
  title = 'my-component';

  @VLog count = 0;

  constructor(){
    //
  }

  ngOnInit(): void {
    // console.log('my ngOnInit');

    this.visit();
  }

  visit(): void {
    console.log('visit...');
  }

  @MLog()
  add(){
    this.count++;
  }
}

注意:

Angular中的装饰器和ES7中的装饰器有些不同:

  • ES7中的class装饰器第一个参数是class本身,类方法装饰器第一个参数是class的prototype;
  • Angular的类方法装饰器和属性装饰器的第一个参数都是类实例;
  • Angular比ES7多了属性装饰器;

总结:

Angular中的装饰器功能更加强大。

React中的装饰器模式

为了提高组件复⽤用率,可测试性,就要保证组件功能单一性;但是若要满⾜足复杂需求就要扩展功能单一的组件,在React里就有了HOC(Higher-Order Components)的概念。 高阶组件时参数为组件,返回值为新组件的函数。

import React, { Component, PureComponent } from 'react'

function HOCTest(Comp){
    return function(props){
        return (<div style={{ border: '1px solid green'}}>
            <Comp {...props}/>
        </div>);
    }
}

export default HOCTest(function CachePage(props){
    return (<div>
        Tom
    </div>);
})

HOC通常实现为一个函数,本质上是抽象工厂模式。 高阶组件本身是对装饰器模式的应用,自然可以利用ES7中出现的装饰器语法来更优雅的书写代码。

@HOCTest
function CachePage(props){
    return (<div>
        Tom
    </div>);
}

优点缺点

优点

装饰类和被装饰类可以独立发展,不会相互耦合; 装饰模式是继承的一个替代模式; 装饰模式可以动态扩展一个实现类的功能,而不必担心影响实现类;

缺点

如果管理不当会极大增加系统复杂度; 多层装饰比较复杂;

设计原则

装饰器模式将现有对象和装饰器进行分离,两者独立存在,符合开放封闭原则

我的微信公众号

更多精彩文章请关注我的前端技术公众号哦!

wechat.jpg