JavaScript设计模式-装饰者模式

概念

  在程序开发中,许多时候并不希望某个类天生就非常庞大,一次性包含许多职责。我们可以使用装饰者模式。装饰者模式可以动态地给某个对象添加一些额外的职责,而不会影响从这个类中派生的其他对象。
  在传统的面向对象语言中,给对象添加功能常常使用继承的方式,但是继承的方式并不灵活,还会带来许多问题:一方面会导致超类和子类之间存在强耦合性,当超类改变时,子类也会随之改变;另一方面,继承这种功能复用方式通常被称为“白箱复用”,“白箱”是相对可见性而言的,在继承方式中,超类的内部细节是对子类可见的,继承常常被认为破坏了封装性。
  使用继承还会带来另外一个问题,在完成一些功能复用的同时,有可能创建出大量的子类,使子类的数量呈爆炸性增长。比如现在有4种型号的自行车,为每种自行车都定义了一个单独的类。现在要给每种自行车都装上前灯、尾灯和铃铛这3种配件。如果使用继承的方式来给每种自行车创建子类,则需要4×3=12个子类。但是如果把前灯、尾灯、铃铛这些对象动态组合到自行车上面,则只需要额外增加3个类。
  这种给对象动态地增加职责的方式称为装饰者(decorator)模式。装饰者模式能够在不改变对象自身的基础上,在程序运行期间给对象动态地添加职责。跟继承相比,装饰者是一种更轻便灵活的做法,这是一种“即用即付”的方式,比如天冷了就多穿一件外套,需要飞行时就在头上插一支竹蜻蜓,遇到一堆食尸鬼时就点开AOE(范围攻击)技能。
  作为一门解释执行的语言,给JS中的对象动态添加或者改变职责是一件再简单不过的事情。

描述

  假设在编写一个飞机大战的游戏,随着经验值的增加,操作的飞机对象可以升级成更厉害的飞机,一开始这些飞机只能发射普通的子弹,升到第二级时可以发射导弹,升到第三级时可以发射原子弹。
  我们先用面向对象编程形式用装饰者模式实现。首先是原始的飞机类:

var Plane = function(){};

Plane.prototype.fire = function(){
    console.log( '发射普通子弹' );
}
var MissileDecorator = function( plane ){
    this.plane = plane;
}
MissileDecorator.prototype.fire = function(){
    this.plane.fire();
    console.log( '发射导弹' );
}
var AtomDecorator = function( plane ){
    this.plane = plane;
}
AtomDecorator.prototype.fire = function(){
    this.plane.fire();
    console.log( '发射原子弹' );
}

  JS语言动态改变对象相当容易,可以直接改写对象或者对象的某个方法,并不需要使用“类”来实现装饰者模式。

var plane = {
    fire: function(){
        console.log( '发射普通子弹' );
    }
}
var missileDecorator = function(){
    console.log( '发射导弹' );
}
var atomDecorator = function(){
    console.log( '发射原子弹' );
}
var fire1 = plane.fire;
plane.fire = function(){
    fire1();
    missileDecorator();
}
var fire2 = plane.fire;
plane.fire = function(){
    fire2();
    atomDecorator();
}
plane.fire();
// 分别输出: 发射普通子弹、发射导弹、发射原子弹

应用

装饰函数

  在JS中可以很方便地给某个对象扩展属性和方法,但却很难在不改动某个函数源代码的情况下,给该函数添加一些额外的功能。在代码的运行期间,很难切入某个函数的执行环境。要想为函数添加一些功能,最简单粗暴的方式就是直接改写该函数,但这是最差的办法,直接违反了开放——封闭原则。

var a = function(){
  alert(1);
}
//改成:
var a = function(){
  alert(1);
  alert(2);
}

  很多时候不想去碰原函数,也许原函数是由其他同事编写的,里面的实现非常杂乱。现在需要一个办法,在不改变函数源代码的情况下,能给函数增加功能,通过保存原引用的方式就可以改写某个函数:

var a =  function(){
  alert(1);
}
var _a = a;

a = function(){
  _a();
  alert(2);
}
a();

  这是实际开发中很常见的一种做法,比如想给window绑定onload事件,但是又不确定这个事件是不是已经被其他人绑定过,为了避免覆盖掉之前的window.onload函数中的行为,一般都会先保存好原先的window.onload,把它放入新的window.onload里执行:

window.onload=function(){
  alert(1);
}
var _onload=window.onload||function(){};
window.onload=function(){
  _onload();
  alert(2);
}

  这样的代码是符合开放——封闭原则的,在增加新功能的时候,确实没有修改原来的window.onload代码,但是这种方式存在以下两个问题
  1、必须维护_onload这个中间变量,虽然看起来并不起眼,但如果函数的装饰链较长,或者需要装饰的函数变多,这些中间变量的数量也会越来越多。
  2、遇到了this被劫持的问题,在window.onload的例子中没有这个烦恼,是因为调用普通函数_onload时,this也指向window,跟调用window.onload时一样(函数作为对象的方法被调用时,this指向该对象,所以此处this也只指向window)。现在把window.onload换成document.getElementById,代码如下:

var _getElementById = document.getElementById;
document.getElementById= function(id){
  alert(1);
  return _getElementById(id);    //(1)
}
var button = document.getElementById('button');

  执行这段代码,看到在弹出alert(1)之后,紧接着控制台抛出了异常,异常发生在(1)处的_getElementById(id)这句代码上,此时_getElementById是一个全局函数,当调用一个全局函数时,this是指向window的,而document.getElementById方法的内部实现需要使用this引用,this在这个方法内预期是指向document,而不是window,这是错误发生的原因,所以使用现在的方式给函数增加功能并不保险。
  改进后的代码可以满足需求,要手动把document当作上下文this传入_getElementById:

var _getElementById = document.getElementById;
document.getElementById=function(){
  alert(1);
  return _getElementById.apply(document,arguments);
}
var button = document.getElementById('button');

  但这样做显然很不方便,下面使用AOP来提供一种完美的方法给函数动态增加功能。
  首先给出Function.prototype.before方法和Function.prototype.after方法:

Function.prototype.before = function( beforefn ){
    var __self = this; // 保存原函数的引用
    return function(){ // 返回包含了原函数和新函数的"代理"函数
        beforefn.apply( this, arguments ); // 执行新函数,且保证this 不被劫持,新函数接受的参数
    // 也会被原封不动地传入原函数,新函数在原函数之前执行
        return __self.apply( this, arguments ); // 执行原函数并返回原函数的执行结果,
    // 并且保证this 不被劫持
    }
}
Function.prototype.after = function( afterfn ){
    var __self = this;
    return function(){
        var ret = __self.apply( this, arguments );
        afterfn.apply( this, arguments );
        return ret;
    }
};

  让我们看看怎么用?

<button id="button"></button>
<script>
    Function.prototype.before = function( beforefn ){
        var __self = this;
        return function(){
            beforefn.apply( this, arguments );
            return __self.apply( this, arguments );
        }
    }
    document.getElementById = document.getElementById.before(function(){
        alert (1);
    });
    var button = document.getElementById( 'button' );
    console.log( button );
</script>

  再回到window.onload的例子,用Function.prototype.after来增加新的window.onload事件非常简单。

window.onload = function(){
    alert (1);
}
window.onload = ( window.onload || function(){} ).after(function(){
    alert (2);
}).after(function(){
    alert (3);
}).after(function(){
    alert (4);
});

  值得提到的是,上面的AOP实现是在Function.prototype上添加before和after方法,但许多人不喜欢这种污染原型的方式,那么可以做一些变通,把原函数和新函数都作为参数传入before或者after方法:

var before = function( fn, beforefn ){
    return function(){
        beforefn.apply( this, arguments );
        return fn.apply( this, arguments );
    }
}
var a = before(
    function(){alert (3)},
    function(){alert (2)}
    );
a = before( a, function(){alert (1);} );
a();

小结

  用AOP装饰函数的技巧在实际开发中非常有用。不论是业务代码的编写,还是在框架层面,都可以把行为依照职责分成粒度更细的函数,随后通过装饰把它们合并到一起,这有助于编写一个松耦合和高复用性的系统。

参考文献

《JavaScript设计模式与开发实践》

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

推荐阅读更多精彩内容

  • 工厂模式类似于现实生活中的工厂可以产生大量相似的商品,去做同样的事情,实现同样的效果;这时候需要使用工厂模式。简单...
    舟渔行舟阅读 7,726评论 2 17
  • 装饰者模式:在不改变原对象的基础上,通过对其进行包装扩展,使原有对象可以满足用户的更复杂需求 需求:对表单输入框进...
    蟹老板爱写代码阅读 85评论 1 0
  • 单例模式 适用场景:可能会在场景中使用到对象,但只有一个实例,加载时并不主动创建,需要时才创建 最常见的单例模式,...
    Obeing阅读 2,058评论 1 10
  • 平静的生活,已有多年没写过什么,突然发现《简书》软件,读了朋友的随笔。有进来的冲动。
    追梦友人阅读 66评论 0 0
  • 我挥一挥手 冬日又至了 寒冷如昔 隔如昨夜的梦 一片枯黄 点缀在群绿之间 戏谑着 夏日白首的誓言 风 又起了 卷起...
    恋蒲草阅读 267评论 3 1