# ES7-Decorator-装饰者模式

# 装饰模式

仅仅包装现有的模块,使之 “更加华丽” ,并不会影响原有接口的功能 —— 好比你给手机添加一个外壳罢了,并不影响手机原有的通话、充电等功能;

# 使用 ES7 的 decorator

ES7 中增加了一个 decorator 属性,它借鉴自 Python

下面我们以 钢铁侠 为例讲解如何使用 ES7decorator

以钢铁侠为例,钢铁侠本质是一个人,只是“装饰”了很多武器方才变得那么 NB,不过再怎么装饰他还是一个人。

TB1ex47KXXXXXbmXXXXXXXXXXXX-1024-768.jpg

我们的示例场景是这样的

  • 首先创建一个普通的Man类,它的抵御值 2,攻击力为 3,血量为 3;
  • 然后我们让其带上钢铁侠的盔甲,这样他的抵御力增加 100,变成 102;
  • 让其带上光束手套,攻击力增加 50,变成 53;
  • 最后让他增加“飞行”能力

# 【Demo 1】对方法的装饰:装备盔甲

创建 Man 类:

class Man{
  constructor(def = 2,atk = 3,hp = 3){
    this.init(def,atk,hp);
  }

  init(def,atk,hp){
    this.def = def; // 防御值
    this.atk = atk;  // 攻击力
    this.hp = hp;  // 血量
  }
  toString(){
    return `防御力:${this.def},攻击力:${this.atk},血量:${this.hp}`;
  }
}

var tony = new Man();

console.log(`当前状态 ===> ${tony}`);

// 输出:当前状态 ===> 防御力:2,攻击力:3,血量:3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

代码直接放在 http://babeljs.io/repl/ 中运行查看结果, 记得勾选SettingEvaluate选项,和 options的选项为legacy

创建 decorateArmour 方法,为钢铁侠装配盔甲——注意 decorateArmour 是装饰在方法init上的。

function decorateArmour(target, key, descriptor) {
  const method = descriptor.value;
  let moreDef = 100;
  let ret;
  descriptor.value = (...args)=>{
    args[0] += moreDef;
    ret = method.apply(target, args);
    return ret;
  }
  return descriptor;
}

class Man{
  constructor(def = 2,atk = 3,hp = 3){
    this.init(def,atk,hp);
  }

  @decorateArmour
  init(def,atk,hp){
    this.def = def; // 防御值
    this.atk = atk;  // 攻击力
    this.hp = hp;  // 血量
  }
  toString(){
    return `防御力:${this.def},攻击力:${this.atk},血量:${this.hp}`;
  }
}

var tony = new Man();

console.log(`当前状态 ===> ${tony}`);
// 输出:当前状态 ===> 防御力:102,攻击力:3,血量:3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

我们先看输出结果,防御力的确增加了 100,看来盔甲起作用了。

Decorators 的本质是利用了 ES5Object.defineProperty 属性,这三个参数其实是和 Object.defineProperty 参数一致的

# 【Demo 2】装饰器叠加:增加光束手套

在上面的示例中,我们成功为 普通人 增加 “盔甲” 这个装饰;现在我想再给他增加 “光束手套”,希望额外增加 50 点防御值。

...
function decorateLight(target, key, descriptor) {
  const method = descriptor.value;
  let moreAtk = 50;
  let ret;
  descriptor.value = (...args)=>{
    args[1] += moreAtk;
    ret = method.apply(target, args);
    return ret;
  }
  return descriptor;
}

class Man{
  constructor(def = 2,atk = 3,hp = 3){
    this.init(def,atk,hp);
  }

  @decorateArmour
  @decorateLight
  init(def,atk,hp){
    this.def = def; // 防御值
    this.atk = atk;  // 攻击力
    this.hp = hp;  // 血量
  }
...
}
var tony = new Man();
console.log(`当前状态 ===> ${tony}`);
//输出:当前状态 ===> 防御力:102,攻击力:53,血量:3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

在这里你就能看出装饰模式的优势了,它可以对某个方法进行叠加使用,对原类的侵入性非常小,只是增加一行@decorateLight而已,可以方便地增删;(同时还可以复用)

# 【Demo 3】对类的装饰:增加飞行能力

装饰模式有两种:纯粹的装饰模式 和 半透明的装饰模式。

上述的两个 demo 中所使用的应该是 纯粹的装饰模式,它并不增加对原有类的接口;下面要讲 demo 是给普通人增加“飞行”能力,相当于给类新增一个方法,属于 半透明的装饰模式,有点儿像适配器模式的样子。


...

// 3
function addFly(canFly){
  return function(target){
    target.canFly = canFly;
    let extra = canFly ? '(技能加成:飞行能力)' : '';
    let method = target.prototype.toString;
    target.prototype.toString = function (...args){
      return method.apply(this, args) + extra;
    }
    return target;
  }
}

@addFly(true)
class Man{
  constructor(def = 2,atk = 3,hp = 3){
    this.init(def,atk,hp);
  }

  @decorateArmour
  @decorateLight
  init(def,atk,hp){
    this.def = def; // 防御值
    this.atk = atk;  // 攻击力
    this.hp = hp;  // 血量
  }
  ...
}
...

console.log(`当前状态 ===> ${tony}`);
// 输出:当前状态 ===> 防御力:102,攻击力:53,血量:3(技能加成:飞行能力)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

作用在方法上的 decorator 接收的第一个参数(target )是类的 prototype;如果把一个 decorator 作用到类上,则它的第一个参数 target 是 类本身。

# 使用原生 JS 实现装饰器模式

  • Man 是具体的类,Decorator 是针对 Man 的装饰器基类
  • 具体的装饰类 DecorateArmour 典型地使用 prototype 继承方式 继承自 Decorator 基类;
  • 基于 IOC(控制反转)思想 ,Decorator 是接受 Man 类,而不是自己创建 Man 类;
// 首先我们要创建一个基类
function Man(){

  this.def = 2;
  this.atk = 3;
  this.hp = 3;
}

// 装饰者也需要实现这些方法,遵守 Man 的接口
Man.prototype={
  toString:function(){
    return `防御力:${this.def},攻击力:${this.atk},血量:${this.hp}`;
  }
}
// 创建装饰器,接收 Man 对象作为参数。
var Decorator = function(man){
  this.man = man;
}

// 装饰者要实现这些相同的方法
Decorator.prototype.toString = function(){
    return this.man.toString();
}

// 继承自装饰器对象
// 创建具体的装饰器,也是接收 Man 作对参数
var DecorateArmour = function(man){

  var moreDef = 100;
  man.def += moreDef;
  Decorator.call(this,man);

}
DecorateArmour.prototype = new Decorator();

// 接下来我们要为每一个功能创建一个装饰者对象,重写父级方法,添加我们想要的功能。
DecorateArmour.prototype.toString = function(){
  return this.man.toString();
}

// 注意这里的调用方式
// 构造器相当于“过滤器”,面向切面的
var tony = new Man();
tony = new DecorateArmour(tony);
console.log(`当前状态 ===> ${tony}`);
// 输出:当前状态 ===> 防御力:102,攻击力:3,血量:3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46

# 经典实现:Logger

经典应用就是 日志系统 了,那么我们也用 ES7 的语法给钢铁侠打造一个日志系统吧。

/**
 * Created by jscon on 15/10/16.
 */
let log = (type) => {

  return (target, name, descriptor) => {
    const method = descriptor.value;
    descriptor.value =  (...args) => {
      console.info(`(${type}) 正在执行: ${name}(${args}) = ?`);
      let ret;
      try {
        ret = method.apply(target, args);
        console.info(`(${type}) 成功 : ${name}(${args}) => ${ret}`);
      } catch (error) {
        console.error(`(${type}) 失败: ${name}(${args}) => ${error}`);
      }
      return ret;
    }
  }
}
class IronMan {
  @log('IronMan 自检阶段')
  check(){
    return '检查完毕';
  }
  @log('IronMan 攻击阶段')
  attack(){
    return '击倒敌人';
  }
  @log('IronMan 机体报错')
  error(){
    throw 'Something is wrong!';
  }
}

var tony = new IronMan();
tony.check();
tony.attack();
tony.error();

// 输出:
// (IronMan 自检阶段) 正在执行: check() = ?
// (IronMan 自检阶段) 成功 : check() => 检查完毕
// (IronMan 攻击阶段) 正在执行: attack() = ?
// (IronMan 攻击阶段) 成功 : attack() => 击倒敌人
// (IronMan 机体报错) 正在执行: error() = ?
// (IronMan 机体报错) 失败: error() => Something is wrong!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

Logger 方法的关键在于:

  • 首先使用 const method = descriptor.value; 将原有方法提取出来,保障原有方法的纯净;
  • try..catch 语句是 调用 ret = method.apply(target, args);在调用之前之后分别进行日志汇报;
  • 最后返回 return ret; 原始的调用结果

# 相关库

  • https://github.com/jayphelps/core-decorators

# vue中使用装饰器实现AOP编程

JavaScript中实现AOP,是把一个函数“动态织入”到另一个函数之中。 首先要构造Functionprototype

//prototype.js
Function.prototype.before = function (beforefn) {
  let _self = this;
  return function () {
    beforefn.apply(this, arguments);
    return _self.apply(this, arguments);
  };
};
Function.prototype.after = function (afterfn) {
  let _self = this;
  return function () {
    let ret = _self.apply(this, arguments);
    afterfn.apply(this, arguments);
    return ret;
  };
};
Function.prototype.around = function (beforefn, afterfn) {
  let _self = this;
  return function () {
    beforefn.apply(this, arguments);
    let ret = _self.apply(this, arguments);
    afterfn.apply(this, arguments);
    return ret;
  };
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

编辑我们的装饰器函数

//decorator.js
export const before = function (...args) {
  return function (target, key, descriptor) {
    descriptor.value = descriptor.value.before(() => {
      console.log(`Action-${key} 触发埋点!`);
    });
  };
};
export const after = function (...args) {
  return function (target, key, descriptor) {
    descriptor.value = descriptor.value.after(() => {
      console.log(`Action-${key} 触发埋点!`);
    });
  };
};
export const around = function (...args) {
  return function (target, key, descriptor) {
    descriptor.value = descriptor.value.around(() => {
      console.log(`Action-${key} 触发埋点before!`);
    }, () => {
      console.log(`Action-${key} 触发埋点after!`);
    });
  };
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

编辑我们的vue文件

//test.vue
<template>
  <div></div>
</template>
<script>
import { before, after, around } from '@/lib/decorator';
export default {
  data () {

  },
  methods: {
    @before()
    @after()
    @around()
    errorHandle () {
      // 一些共用的异常处理方案
    }
  },
};
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

.babelrc文件

{
  "plugins": [
    ["@babel/plugin-proposal-decorators", { "legacy": true }]
  ]
}
1
2
3
4
5

# vue中 debounce 和 打点, logger 应用

// 理解JS函数节流和函数防抖 中节流 修改如下
export function debounce(wait = 800, now = true) {
  let __timer = null;
  let isFirst = now;
  return function(target, key, descriptor) {
    const method = descriptor.value;
    descriptor.value = (...args) => {
      if (__timer) clearTimeout(__timer);
      if (isFirst) {
        method.apply(target, args);
        isFirst = false;
      } else {
        __timer = setTimeout(() => {
          method.apply(target, args);
        }, wait);
      }
    };
    return descriptor;
  };
}

export function goTrack(point){
  return function (target, key, descriptor){
    const method = descriptor.value
    let ret = true;
    descriptor.value = function (...args) {
      ret = method.apply(this, args)
      if (ret !== false){
      //  条件 返回 false 不执行
        console.warn('已经打点', point)
      }
      return ret;
    }
    return descriptor;
  };
}
// 写一个 promise
export function pro(){
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve()
    }, 500);
  });
}

export function logger(isAsync = false){
  return function (target, key, descriptor){
    const method = descriptor.value
    let ret = true;
    descriptor.value = function (...args) {
      ret = method.apply(this, args)
      if(isAsync){
        // 是异步
        ret.then(() => {
          console.warn('logger success')
        }).catch(() => {
          console.error('logger fail')
        })
      }else {
        console.warn('logger success')
      }
      return ret;
    }
    return descriptor;
  };
}

// vue 中引入
{
  methods: {
    @debounce(1000, true)
    outputFn (e) {
      console.warn('输出', e.target.value)
    },
    @goTrack('#23856')
    btnClick () {
      if(!this.list.length){
        return false;
      }
      console.log('开始打点')
    },
    @logger(true)
    btnClick2 () {
      return new Promise((resolve, reject) => {
        pro().then(() => {
          console.log('我是异步完成')
          resolve()
        })
      });
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92

# 摘自

# 今日图 羞羞哒

gdfs.gif