javascript设计模式

293 阅读11分钟

单例模式

惰性单例模式封装

var getSingle = function (fn) {
    var result;
    return function () {
        return result || ( result = fn.apply(this, arguments) );
    }
}

策略模式

定义:定义一系列的算法,把他们一个个封装起来,并使他们可以相互替换。 除了算法外,策略模式还可以用来封装一系列的业务规则,例如:表单校验,业务规则可以对应表单校验的规则和错误的提示语。定义好规则策略后,需要校验的地方传入规则名和错误提示即可。

代理模式

定义:代理模式是为一个对象提供一个代用品或占位符,以便控制对它的访问。

保护代理:可以增加条件做过滤,拒绝掉一部分的请求,控制不同权限的对象对目标对象的访问 虚拟代理:对耗时或者可合并的操作等,做延迟处理,在合适的时机触发。例如:加载图片、合并接口请求等。

合并请求的虚拟代理例子:

var synchromousFile = function ( id ) {
    console.log('开始同步文件,id为' + id);
};

var proxySynchronousFile = (function () {
    var cache = [], // 保存一段时间内需要同步的ID集合
    timer;

    return function( id ) {
        cache.push(id);
        if ( timer ) {
            return;
        }

        timer = setTimeout(function() {
            synchromousFile( cache.join(',')); // 2秒后向本体发送需要同步的ID集合
            clearTimeout( timer );
            timer = null;
            cache.length = 0; //清空ID集合
        }, 2000)
    }
})();

发布-订阅模式

定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖它的对象都将得到通知。 优点:一为时间上的解耦,二为对象之间的解耦。 缺点:过多的使用会使得模块之间的联系被隐藏到了背后。

以下为全局的发布-订阅对象:

var Event = (function () {

    var clientList = {},
        listen,
        trigger,
        remove;
    
    listen = function (key, fn) {
        if (!clientList[ key ]) {
            clientList[ key ] = [];
        }
        clientList[ key ].push( fn );
    };

    trigger = function () {
        var key = Array.prototype.shift.call( arguments ),
            fns = clientList[ key ];
            if ( !fns || fns.length === 0) {
                return false;
            }
            for ( var i = 0, fn; fn = fns[ i++ ]; ) {
                fn.apply( this, arguments);
            }
    }

    remove = function ( key, fn ) {
        var fns = clientList[ key ];
        if ( !fns ) {
            return false;
        }
        if ( !fn ) {
            fns && ( fns.length = 0);
        } else {
            for ( var l = fns.length - 1; l >= 0; l--) {
                var _fn = fns[ l ];
                if ( _fn === fn ) {
                    fns.splice( l, 1);
                }
            }
        }
    };

    return {
        listen: listen,
        trigger: trigger,
        remove: remove
    }
})();

命令模式

定义:指的是一个执行某些特定事情的指令。 最常用的场景:有时候需要向某些对象发送请求,但是并不知道请求的接收者是谁,也不知道被请求的操作是什么。此时希望用一种松耦合的方式来设计程序,使得请求发送者和请求接收者能够消除彼此之间的耦合关系。 命令模式支持撤销、排队、宏命令等。

使用闭包的命令模式:

var setCommand = function ( button, func ) {
    button.onclick = function () {
        func();
    }
};

var MenuBar = {
    refresh: function () {
        console.log( '刷新' );
    }
};

var RefreshMenuBarCommand = function ( receiver ) {
    return function () {
        receiver.refresh();
    }
};

var refreshMenuBarCommand = RefreshMenuBarCommand( MenuBar );

setCommand( button1, refreshMenuBarCommand);

组合模式

组合模式将对象组合成树形结构,以表示“部分-整体”的层次结构。除了用来表示树形结构之外,组合模式的另一个好处是通过对象的多态性表现,使得用户对单个对象和组合对象的使用具有一致性,下面分别说明。

  • 表示树形结构。通过回顾上面的例子,我们很容易找到组合模式的一个优点:提供了一种遍历树形结构的方案,通过调用组合对象的execute方法,程序会递归调用组合对象下面的叶对象的execute方法,所以我们的万能遥控器只需要一次操作,便能依次完成关 门、打开电脑、登录QQ这几件事情。组合模式可以非常方便地描述对象部分-整体层次结构。
  • 利用对象多态性统一对待组合对象和单个对象。利用对象的多态性表现,可以使客户端忽略组合对象和单个对象的不同。在组合模式中,客户将统一地使用组合结构中的所有对象,而不需要关心它究竟是组合对象还是单个对象。
// 创建一个宏命令
var MacroCommand = function(){
    return {
        // 宏命令的子命令列表
        commandsList: [],
        // 添加命令到子命令列表
        add: function( command ){
            this.commandsList.push( command );
        },
        // 依次执行子命令列表里面的命令
        execute: function(){
            for ( var i = 0, command; command = this.commandsList[ i++ ]; ){
                command.execute();
            }
        }
    }
};

<!--打开空调命令-->
var openAcCommand = {
    execute: function(){
        console.log( '打开空调' );
    }
};

<!--打开电视和音响-->
var openTvCommand = {
    execute: function(){
        console.log( '打开电视' );
    }
};
var openSoundCommand = {
    execute: function(){
        console.log( '打开音响' );
    }
};
//创建一个宏命令
var macroCommand1 = MacroCommand();
//把打开电视装进这个宏命令里
macroCommand1.add(openTvCommand)
//把打开音响装进这个宏命令里
macroCommand1.add(openSoundCommand)

<!--关门、打开电脑和打登录QQ的命令-->
var closeDoorCommand = {
    execute: function(){
        console.log( '关门' );
    }
};
var openPcCommand = {
    execute: function(){
        console.log( '开电脑' );
    }
};
var openQQCommand = {
    execute: function(){
        console.log( '登录QQ' );
    }
};
//创建一个宏命令
var macroCommand2 = MacroCommand();
//把关门命令装进这个宏命令里
macroCommand2.add( closeDoorCommand );
//把开电脑命令装进这个宏命令里
macroCommand2.add( openPcCommand );
//把登录QQ命令装进这个宏命令里
macroCommand2.add( openQQCommand );

<!--把各宏命令装进一个超级命令中去-->
var macroCommand = MacroCommand();
macroCommand.add( openAcCommand );
macroCommand.add( macroCommand1 );
macroCommand.add( macroCommand2 );

享元模式

享元模式的核心是运用共享技术来有效支持大量细粒度的对象,如果系统中因为创建了大量类似的对象而导致内存占用过高,享元模式就非常有用。

享元模式要求对象的属性划分为内部状态和外部状态,如何划分:

  • 内部状态存储于对象内部
  • 内部状态可以被一些对象共享
  • 内部状态独立于具体的场景,通常不会变化
  • 外部状态取决于具体的场景,并根据场景而变化,外部状态不能被共享。

示例:文件上传

var Upload = function(uploadType) {
  this.uploadType = uploadType;
}
​
/* 删除文件(内部状态) */
Upload.prototype.delFile = function(id) {
  uploadManger.setExternalState(id, this);  // 把当前id对应的外部状态都组装到共享对象中
  // 大于3000k提示
  if(this.fileSize < 3000) {
    return this.dom.parentNode.removeChild(this.dom);
  }
  if(window.confirm("确定要删除文件吗?" + this.fileName)) {
    return this.dom.parentNode.removeChild(this.dom);
  }
}

/** 工厂对象实例化 
 *  如果某种内部状态的共享对象已经被创建过,那么直接返回这个对象
 *  否则,创建一个新的对象
 */
var UploadFactory = (function() {
    var createdFlyWeightObjs = {};
    return {
        create: function(uploadType) {
            if(createdFlyWeightObjs[ uploadType ]) {
                return createdFlyWeightObjs[ uploadType ]
            }
            return createdFlyWeightObjs[ uploadType ] = new Upload( uploadType );
        }
    }
})();

/* 管理器封装外部状态 */
var uploadManger = (function() {
  var uploadDatabase = {};
​
  return {
    add: function(id, uploadType, fileName, fileSize) {
      var flyWeightObj = UploadFactory.create(uploadType);
      var dom = document.createElement('div');
      dom.innerHTML = "<span>文件名称:" + fileName + ",文件大小:" + fileSize +"</span>"
              + "<button class='delFile'>删除</button>";
​
      dom.querySelector(".delFile").onclick = function() {
        flyWeightObj.delFile(id);
      };
      document.body.appendChild(dom);
​
      uploadDatabase[id] = {
        fileName: fileName,
        fileSize: fileSize,
        dom: dom
      };
​
      return flyWeightObj;
    },
    setExternalState: function(id, flyWeightObj) {
      var uploadData = uploadDatabase[id];
      for(var i in uploadData) {
        flyWeightObj[i] = uploadData[i];
      }
    }
  };
})();

/*触发上传动作*/
var id = 0;
window.startUpload = function(uploadType, files) {
  for(var i=0,file; file = files[i++];) {
    var uploadObj = uploadManger.add(++id, uploadType, file.fileName, file.fileSize);
  }
};
​
/* 测试 */
startUpload("plugin", [
  {
    fileName: '1.txt',
    fileSize: 1000
  },{
    fileName: '2.txt',
    fileSize: 3000
  },{
    fileName: '3.txt',
    fileSize: 5000
  }
]);
startUpload("flash", [
  {
    fileName: '4.txt',
    fileSize: 1000
  },{
    fileName: '5.txt',
    fileSize: 3000
  },{
    fileName: '6.txt',
    fileSize: 5000
  }
]);

职责链模式

使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系,将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。

例子:异步职责链的实现

// Chain.prototype.setNextSuccessor 指定在链中的下一个节点
// Chain.prototype.passRequest 传递请求给某个节点
var Chain = function ( fn ) {
    this.fn = fn;
    this.successor = null;
}
Chain.prototype.setNextSuccessor = function ( successor ) {
    return this.successor = successor;
}

// 对于ret的判断可以用于同步的职责链模式
Chain.prototype.passRequest = function () {
    var ret = this.fn.apply( this, arguments);
    if ( ret === 'nextSuccessor' ) {
        return this.successor && this.successor.passRequest.apply( this.successor, arguments );
    }
    return ret;
}

// 异步时ret的return值没有,此时无法接收,因此采用next回调的方式
Chain.prototype.next = function () {
    return this.nextSuccessor && this.successor.passRequest.apply( this.successor, arguments );
}

var fn1 = new Chain(function(){
    console.log( 1 );
    return 'nextSuccessor';
});
var fn2 = new Chain(function(){
    console.log( 2 );
    var self = this;
    setTimeout(function(){
        self.next();
    }, 1000 );
});
var fn3 = new Chain(function(){
    console.log( 3 );
});
fn1.setNextSuccessor( fn2 ).setNextSuccessor( fn3 );
fn1.passRequest();

用AOP实现职责链

Function.prototype.after = function( fn ){
    var self = this;
    return function(){
        var ret = self.apply( this, arguments );
        if ( ret === 'nextSuccessor' ){
            return fn.apply( this, arguments );
        }
        return ret;
    }
};

// 此处的order500yuan、order200yuan、orderNormal为订单处理的函数,此处未列出
var order = order500yuan.after( order200yuan ).after( orderNormal );
order( 1, true, 500 ); // 输出:500元定金预购,得到100优惠券
order( 2, true, 500 ); // 输出:200元定金预购,得到50优惠券
order( 1, false, 500 ); // 输出:普通购买,无优惠券

中介者模式

中介者模式的作用就是解除对象与对象之间的紧耦合关系。增加一个中介者对象后,所有的相关对象都通过中介者对象来通信,而不是互相引用,所以当一个对象发生改变时,只需要通知中介者对象即可。中介者使各对象之间耦合松散,而且可以独立地改变它们之间的交互。

例子:游戏中使用中介者控制游戏状态

玩家对象的生成:

function Player (name, teamColor) {
    this.name = name;
    this.teamColor = teamColor;
    this.state = 'alive';
};

Player.prototype.win = function () {
    console.log(`${this.name} won!`);
};

Player.prototype.lose = function () {
    console.log(`${this.name} lost!`);
};
// 玩家死亡
Player.prototype.die = function () {
    this.state = 'dead';
    // 给中介者发送消息,玩家死亡
    playerDirector.ReceiveMessage( 'playDead', this );
}

// 移除玩家
Player.prototype.remove = function () {
    // 给中介者发送消息,玩家移除
    playerDirector.ReceiveMessage( 'removePlayer', this );
}

// 玩家换队
Player.prototype.remove = function ( color ) {
    // 给中介者发送消息,玩家换队
    playerDirector.ReceiveMessage( 'changeTeam', this, color );
}

// 生成玩家的工厂函数
var palyerFactory = function ( name, teamColor ) {
    var newPlayer = new Player( name, teamColor );

    // 给中介者发动消息,新增玩家
    playerDirecotor.ReceiveMessage( 'addPlayer', newPlayer );
    return newPlayer;
}

中介者playerDirector的实现:

  • 利用发布—订阅模式。将playerDirector实现为订阅者,各player作为发布者,一旦player的状态发生改变,便推送消息给playerDirector,playerDirector处理消息后将反馈发送给其他player。
  • 在playerDirector中开放一些接收消息的接口,各player可以直接调用该接口来给playerDirector发送消息,player只需传递一个参数给playerDirector,这个参数的目的是使playerDirector可以识别发送者。同样,playerDirector接收到消息之后会将处理结果反馈给其他player。

以下采取开发接口的方式:

var playerDirector = ( function () {
    var players = {}, // 保存所有玩家
        operations = {}; // 中介者可以执行的操作

    // 新增一个玩家
    operations.addPlayer = function ( player ) {
        var teamColor = player.teanmColor;
        players[ teamColor ] = players[ teamColor ] || [];
        players[ teamColor ].push( player );
    }

    // 移除一个玩家
    operations.removePlayer = function ( player ) {
        var teamColor = player.teamColor,
            teamPlayers = players[ teamColor ] || []
        for ( var i = teamPlayers.length - 1; i >= 0; i-- ){   // 遍历删除
            if ( teamPlayers[ i ] === player ){
                teamPlayers.splice( i, 1 );
            }
        }
    }

    // 玩家换队
    operations.changeTeam = function ( player, newTeamColor ) {
        operations.removePlayer( player );
        player.teamColor = newTeamColor;
        operations.addPlayer( player );
    }

    // 玩家死亡
    operations.playerDead = function( player ){     // 玩家死亡
        var teamColor = player.teamColor,
            teamPlayers = players[ teamColor ];   // 玩家所在队伍

        var all_dead = true;

        for ( var i = 0, player; player = teamPlayers[ i++ ]; ){
            if ( player.state !== 'dead' ){
                all_dead = false;
                break;
            }
        }

        if ( all_dead === true ){   // 全部死亡

            for ( var i = 0, player; player = teamPlayers[ i++ ]; ){
                player.lose();   // 本队所有玩家lose
            }

            for ( var color in players ){
                if ( color !== teamColor ){
                    var teamPlayers = players[ color ];   // 其他队伍的玩家
                    for ( var i = 0, player; player = teamPlayers[ i++ ]; ){
                        player.win();   // 其他队伍所有玩家win
                    }
                }
            }
        }
    };

    // 开发接口
    var ReceiveMessage = function () {
        // 第一个参数为消息的类型
        var message = Array.prototype.shift.call( arguments );
        operations[ message ].apply( this, arguments );
    }

    return {
        ReceiveMessage: ReceiveMessage
    }
})();

装饰者模式

装饰者模式能够在不改变对象自身的基础上,在程序运行期间给对象动态的添加职责。

一个简单的例子:

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

var _a = a;

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

a();

用AOP装饰函数

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;
    }
}

// 使用
window.onload = function(){
    alert (1);
}

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

装饰者模式和代理模式

代理模式和装饰者模式最重要的区别在于它们的意图和设计目的。代理模式的目的是,当直接访问本体不方便或者不符合需要时,为这个本体提供一个替代者。本体定义了关键功能,而代理提供或拒绝对它的访问,或者在访问本体之前做一些额外的事情。装饰者模式的作用就是为对象动态加入行为。换句话说,代理模式强调一种关系(Proxy与它的实体之间的关系),这种关系可以静态的表达,也就是说,这种关系在一开始就可以被确定。而装饰者模式用于一开始不能确定对象的全部功能时。代理模式通常只有一层代理-本体的引用,而装饰者模式经常会形成一条长长的装饰链。

状态模式

定义:允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。 我们以逗号分割,把这句话分为两部分来看。第一部分的意思是将状态封装成独立的类,并将请求委托给当前的状态对象,当对象的内部状态改变时,会带来不同的行为变化。 第二部分是从客户的角度来看,我们使用的对象,在不同的状态下具有截然不同的行为,这个对象看起来是从不同的类中实例化而来的,实际上这是使用了委托的效果。

状态模式的通用结构

var Light = function(){
    // 实例化每一个状态对象
    this.offLightState = new OffLightState( this );    // 持有状态对象的引用
    this.weakLightState = new WeakLightState( this );
    this.strongLightState = new StrongLightState( this );
    this.superStrongLightState = new SuperStrongLightState( this );
    this.button = null;
};
Light.prototype.init = function () {
    var button = document.createElement( 'button' ),
        self = this;
    this.button = document.body.appendChild( button );
    this.button.innerHTML = '开关';
    this.currState = this.offLightState; // 设置默认状态

    this.button.onclick = function () {
        self.currState.buttonWasPressed();
    }
}

// 列举一个状态对象,关灯状态对象
var OfflightState = function( light ) {
    this.light = light;
}

OfflightState.prototypr.buttonWasPressed = function () {
    console.log('弱光');
    this.light.setState( this.light.weakLightState );
}

状态机:使用闭包实现将客户的操作委托给状态对象,此委托需要处理this的劫持

var delegate = function ( client, delegation ) {
    return {
        buttonWasPressed: function () {
            return delegation.buttonWasPressed.apply( client, arguments);
        }
    }
};

var FSM = {
    off: {
        buttonWasPressed: function(){
            console.log( '关灯' );
            this.button.innerHTML = '下一次按我是开灯';
            this.currState = this.onState;
        }
    },
    on: {
        buttonWasPressed: function(){
            console.log( '开灯' );
            this.button.innerHTML = '下一次按我是关灯';
            this.currState = this.offState;
        }
    }
};

var Light = function () {
    this.offState = delegation( this, FSM.off );
    this.onState = delegation( this, FSM.on );
    this.currState = this.offState;
    this.button = null;
}

Light.prototype.init = function () {
    var button = document.createElement( 'button' );
    button.innerHTML = '已关灯';
    this.button = document.body.appendChild( button );
    this.button.onclick = function () {
        self.currState.buttonWasPressed();
    }
}

var light = new Light();
light.init();

适配器模式

适配器模式的作用是解决两个软件实体间的接口不兼容的问题。使用适配器模式之后,原本由于接口不兼容而不能工作的两个软件实体可以一起工作。 实际项目中封装组件时,经常遇到组件定义的接口和后台返回的数据无法完全对应上,这时一个apater函数转化数据是比较方便的选择。