2018-07-10装饰器

装饰器模式

装饰模式和适配器模式都是 包装模式 (Wrapper Pattern),它们都是通过封装其他对象达到设计的目的的,但是它们的形态有很大区别。

  • 适配器模式我们使用的场景比较多(继承方案),比如连接不同数据库的情况,你需要包装现有的模块接口,从而使之适配数据库 —— 好比你手机使用转接口来适配插座那样;

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


    更多区别参见:设计模式——装饰模式(Decorator)

装饰模式场景 —— 面向 AOP 编程

装饰模式经典的应用是 AOP 编程,比如“日志系统”,日志系统的作用是记录系统的行为操作,它在不影响原有系统的功能的基础上增加记录环节 —— 好比你佩戴了一个智能手环,并不影响你日常的作息起居,但你现在却有了自己每天的行为记录。

更加抽象的理解,可以理解为给数据流做一层filter,因此 AOP 的典型应用包括 安全检查、缓存、调试、持久化等等。可参考Spring aop 原理及各种应用场景

我们知道,面向对象的特点是继承、多态和封装。而封装就要求将功能分散到不同的对象中去,这在软件设计中往往称为职责分配。实际上也就是说,让不同的类设计不同的方法。这样代码就分散到一个个的类中去了。这样做的好处是降低了代码的复杂程度,使类可重用。
但是人们也发现,在分散代码的同时,也增加了代码的重复性。什么意思呢?比如说,我们在两个类中,可能都需要在每个方法中做日志。按面向对象的设计方法,我们就必须在两个类的方法中都加入日志的内容。也许他们是完全相同的,但就是因为面向对象的设计让类与类之间无法联系,而不能将这些重复的代码统一起来。
也许有人会说,那好办啊,我们可以将这段代码写在一个独立的类独立的方法里,然后再在这两个类中调用。但是,这样一来,这两个类跟我们上面提到的独立的类就有耦合了,它的改变会影响这两个类。那么,有没有什么办法,能让我们在需要的时候,随意地加入代码呢?这种在运行时,动态地将代码切入到类的指定方法、指定位置上的编程思想就是面向切面的编程。

什么是装(修)饰器

装饰器Decorator是在es2016(es7)中新加入的提案,前端中的装饰器的最早在Angular 2+中使用,当然,在当时Angular中,装饰器因TypeScript能使用。

简单来说,装饰器是用一个代码包装另一个代码的简单方式。装饰器是一个函数,主要来修改类的或者类的方法行为,只在类的范畴内有用

如何使用JavaScript装饰器

JavaScript中装饰器使用特殊的语法,使用@作为标识符,且放置在被装饰代码之前。

下面是一个简单的装饰器

function testable(target) {
    target.UserName = 'linxiaodong';
}
@testable
class Test{
}
console.log(Test.UserName); //linxiaodong 

给Test类天津爱了一个静态属性UserName.

装饰器的作用

装饰器的本质只是一种语法糖而已,编译时会把注解的代码翻译成我们熟悉的那种形式。

详解

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

对类的修饰

上面的就是一个对类的修饰,但是如果需要参数的话,我们可以在修饰器外面再封装一层函数。

function testableWrap(username){
    return function testable(target) {
        target.UserName = username;
    }
}

@testableWrap('linxiaodong')
class Test{
}
console.log(Test.UserName); //linxiaodong 

上面是对类添加静态方法,如果要给类的实例对象添加方法,可以通过prototype实现。

 function mixins(...list) {
    return function (target) {
      Object.assign(target.prototype, ...list)
    }
  }
  
  const Foo = {
    foo() { console.log('foo') }
  };
  
  @mixins(Foo)
  class MyClass {}
  
  let obj = new MyClass();
  obj.foo() // 'foo'

上面代码通过修饰器mixins,把Foo对象的方法添加到了MyClass的实例上面。可以用Object.assign()模拟这个功能。

对类中方法的修饰

class Person {
    @readonly
    testFn() { return `${this.first} ${this.last}` }
}
console.log('Person.prototype',Person.prototype);
function readonly(target, name, descriptor){
    console.log('target',target);
    console.log('name',name); // testFn
    console.log('descriptor',descriptor);
    descriptor.writable = false;
    return descriptor;
}

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

修饰器不能用于函数

var counter = 0;
var add = function () {
  counter++;
};
@add
function foo() {
}
//SyntaxError: Leading decorators must be attached to a class declaration

由于存在函数提升,使得修饰器不能用于函数。类是不会提升的,所以就没有这方面的问题。

另一方面,如果一定要修饰函数,可以采用高阶函数的形式直接执行。

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 myDecorator = loggingDecorator(doSomething);
class Test{
    @myDecorator('linxiaodong')
    speak(){
        console.log('speakFn');
    }
}
// Starging
// Hello,linxiaodong
// Finished

常用的装饰器

core-decorators.js是一个第三方模块,提供了几个常见的修饰器,通过它可以更好地理解修饰器。比如Angular2+中就内置了这些装饰器。

(1)@autobind
autobind修饰器使得方法中的this对象,绑定原始对象。

import { autobind } from 'core-decorators';

class Person {
  @autobind
  getPerson() {
    return this;
  }
}

let person = new Person();
let getPerson = person.getPerson;

getPerson() === person;
// true

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

// 首先我们要创建一个基类
function Man(){

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

// 装饰者也需要实现这些方法,遵守 Man 的接口
Man.prototype={
 toString:function(){
 return `防御力:$,攻击力:$,血量:$`;
 }
}
// 创建装饰器,接收 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(`当前状态 ===> $`);
// 输出:当前状态 ===> 防御力:102,攻击力:3,血量:3

下面是一个使用原生和使用装饰器的写法对比

// function decorateArmour(target, key, descriptor) {
//     const method = descriptor.value;
//     console.log(method);
//     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.toString()}`); 


// 使用原生来写
// 首先我们要创建一个基类
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.toString()}`);
   // 输出:当前状态 ===> 防御力:102,攻击力:3,血量:3

参考文献

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,425评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,058评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,186评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,848评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,249评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,554评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,830评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,536评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,239评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,505评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,004评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,346评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,999评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,060评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,821评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,574评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,480评论 2 267

推荐阅读更多精彩内容