js设计模式笔记 - 装饰者模式

从实例中入手

我们就拿《head first 设计模式》中的一个例子入手,使用面向对象的编程思想:

我们有一家咖啡店,暂定每杯咖啡售价10元

function Coffee() {
    this.price = 10
}

Coffee.prototype.getPrice = function () {
    return this.price
}

var coffee = new Coffee()
console.log(coffee.getPrice());

现在新进了一批配料(摩卡,蓝莓,奶泡)分别对应价钱为(5,3,4),那么我们要怎么计算一杯摩卡咖啡的价钱呢?

1.新建一个摩卡咖啡的对象:

function MochaCoffee() {
    this.price = 15
}
MochaCoffee.prototype.getPrice = function () {
    return this.price
}
var mochaCoffee = new MochaCoffee()
console.log(mochaCoffee.getPrice());

2.使用继承

var coffee = new Coffee()

function MochaCoffee() {
    this.mochaPrice = 5
}
MochaCoffee.prototype = new Coffee()
MochaCoffee.prototype.getPrice = function () {
    return this.price + this.mochaPrice
}
var mochaCoffee = new MochaCoffee()
console.log(mochaCoffee.getPrice());

那么每增加一种配料,就需要生成多一个类。

但是,无论是第一种办法,还是第二种办法都很难满足一下的情况,比如

  1. 如果我需要一杯摩卡加蓝莓的咖啡,那么只能再生成一个新的类(MochaBlueberryCoffee)
  2. 如果我需要一杯两倍蓝莓的咖啡呢? 难道要生成一个新的类叫(DoubelMochaBlueberryCoffee)吗?

目前只有3种配料,我们就需要添加3个新的类了,那么当我们的配料添加,或者添加(咖啡根据大中小杯去计算价钱)这种需求时,就需要添加无数的类去实现它;

如果只是使用继承,那么当情况复杂时,类的数量就会呈爆炸式增长,这是我们所不希望见到的。

装饰者模式登场

使用构造器存储coffee对象的引用

function Mocha(coffee) {
    this.price = 5
    this.coffee = coffee
}
Mocha.prototype.getPrice = function () {
    return this.price + this.coffee.getPrice()
}

var coffee = new Coffee()
var mochaCoffee = new Mocha(coffee)
console.log(mochaCoffee.getPrice());

如果这时我们需要点一份双倍摩卡的咖啡,直接如下调用:

var coffee = new Coffee()
var mochaCoffee = new Mocha(coffee)
var doubelMochaCoffee = new Mocha(mochaCoffee)
console.log(doubelMochaCoffee.getPrice());

使构造器存储coffee对象的引用一点也不灵活,我们做如下改动:

function Mocha() {
    this.price = 5
}
Mocha.prototype.getPrice = function () {
    return this.price + this.coffee.getPrice()
}
Mocha.prototype.setCoffee = function (coffee) {
    this.coffee = coffee
}

var coffee = new Coffee()
var mochaCoffee = new Mocha()
mochaCoffee.setCoffee(coffee)
console.log(mochaCoffee.getPrice());

现在就可以动态的设置coffee对象了!

如果你还想要一杯摩卡蓝莓咖啡也是非常容易实现的:

function Blueberry() {
    this.price = 3
}
Blueberry.prototype.getPrice = function () {
    return this.price + this.coffee.getPrice()
}
Blueberry.prototype.setCoffee = function (coffee) {
    this.coffee = coffee
}

var coffee = new Coffee()
var mochaCoffee = new Mocha()
mochaCoffee.setCoffee(coffee)
var mochaBlueberryCoffee = new Blueberry()
mochaBlueberryCoffee.setCoffee(mochaCoffee)
console.log(mochaBlueberryCoffee.getPrice());

简单应用

window.onload = function () {
    alert(1)
}

var _onload = window.onload

window.onload = function () {
    _onload()
    alert('new')
}

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

但是上面的做法会导致this丢失的问题,这里不会报错是因为全局变量就是window;

使用AOP装饰函数

分别实现Function.prototype.beforeFunction.prototype.after,如下:

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

在实际中的应用:

数据统计上报功能:

<html>
    <button tag="login" id="button">点击打开登录浮层</button> 
<script>
    var showLogin = function(){ 
        console.log( '打开登录浮层' ); 
        log('上报信息');
    }
    var log = function( tag ){
        console.log( '上报信息为: ' + tag );
    }
    document.getElementById( 'button' ).onclick = showLogin;
</script> 
</html>

我们看到在 showLogin 函数里,既要负责打开登录浮层,又要负责数据上报,这是两个层面 的功能,在此处却被耦合在一个函数里。使用 AOP 分离之后,代码如下:

<html>
    <button tag="login" id="button">点击打开登录浮层</button> 
<script>
    Function.prototype.after = function( afterfn ){ 
        var __self = this;
        return function(){
            var ret = __self.apply( this, arguments ); 9 afterfn.apply( this, arguments );
            return ret;
        } 
    };
    var showLogin = function(){ 
        console.log( '打开登录浮层' );
    }
    var log = function(){
        console.log( '上报信息为: ' + 'balabalabla' );
    }
    showLogin = showLogin.after( log ); // 打开登录浮层之后上报数据
    document.getElementById( 'button' ).onclick = showLogin;
</script> 
</html>

本质

在《设计模式》成书之前,GoF原想把装饰者(decorator)模式称为包装器(wrapper)模式。
其实装饰者模式,就是在原有的基础上添加了一层包装,形成一条包装链。
使用了装饰者模式,我们可以动态的添加被装饰者的职责,对它进行非入侵式的行为修改,从而达到我们要求而不需要创建更多的类。

缺点:当装饰的层数过深时,实际上作用域链也就变的很长,性能也会受到影响。

参考资料

  • 《head first 设计模式》
  • 《JavaScript设计模式与实践》
  • 《JavaScript设计模式》
    原文作者:an_l
    原文地址: https://segmentfault.com/a/1190000015590639
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞