JS设计模式(一)

241 阅读6分钟

注:将不变的部分和变化的部分分离是每个设计模式的主题

1.单例模式

定义:保证一个类仅有一个实例,并提供一个访问它的全局访问点。

核心:确保只有一个实例,并提供全局访问。

重点:惰性单例,在需要的时候才创建对象实例。

常用的设计模式,有一些对象往往只需要一个,比如线程池、全局缓存、浏览器中的window对象等。

具体案例:单击登录按钮时,页面出现一个登录浮窗,无论单击多少次,这个浮窗只会创建一次,所以这个登录浮窗适合用单例模式创建。

Js中常把全局变量当成单例使用,它满足单例的两个条件,但全局变量不是单例模式。

减少使用全局变量,因为它容易造成命名污染。

1.使用命名空间

2.使用闭包封装私有变量

通用惰性单例:

// 创建登录浮窗的案例
// 管理单例部分,用变量判断是否已经创建对象,已经创建则直接返回
var getSingle = function( fn ){
   var result;
   return function(){
       return result || ( result = fn .apply(this, arguments ) );
   } 
};
// 创建单例部分
var createLoginLayer = function(){
    var div = document.createElement( 'div' );
    div.innerHTML = '我是登录浮窗';
    div.style.display = 'none';  
    document.body.appendChild( div );
    return div;
};

//调用
var createSingleLoginLayer = getSingle( createLoginLayer );

document.getElementById( 'loginBtn' ).onclick = function(){ 
    var loginLayer = createSingleLoginLayer(); 
    loginLayer.style.display = 'block';
};

2.策略模式

定义:定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。

目的:将算法的使用与实现分离开来。

组成:

1.策略类:封装具体的算法,并负责具体的计算过程

2.环境类:接受客户请求,将请求委托给某一个策略类


Js中函数也是对象,常把策略类和环境类直接定义为函数。

// 奖金计算案例
//策略类
var strategies = {
    "S": function( salary ){
         return salary * 4;
     },
    "A": function( salary ){ 
         return salary * 3;
    },
    "B": function( salary ){
         return salary * 2;
    }
};
//环境类
var calculateBonus = function( level, salary ){ 
    return strategies[ level ]( salary );
};
//调用
calculateBonus( 'S', 20000 );  // 输出:80000
calculateBonus( 'A', 10000 );  // 输出:30000

//在js语言中,策略模式是隐形的。以下也是策略模式
var S =  function( salary ) {
         return salary * 4;
};
var A =  function( salary ) {
         return salary * 3;
};
var B =  function( salary ) {
         return salary * 2;
};

var calculateBonus = function( func, salary ){ 
    return func( salary );
}; 
calculateBonus( 'S', 10000 )  // 输出:40000

3.代理模式

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

关键:当客户不方便直接访问一个对象或不满足需要的时候,使用代理来控制对这个对象的访问。

类型:

1.保护代理:代理可以帮助本体过滤掉一些请求,用于控制不同权限的对象对目标对象的访问。

2.虚拟代理(最常用):把一些开销很大的对象,延迟到真正需要它的时候才去创建。

3.缓存代理:为一些开销大的运算结果提供暂时存储,下次运算时,如果参数与之前一致,则返回存储的运算结果。如计算乘积、ajax异步请求数据等。


//虚拟代理实现图片预加载的案例
//本体设置src
var myImage = (function(){
    var imgNode = document.createElement( 'img' );     
    document.body.appendChild( imgNode );
    return {
        setSrc: function( src ){
             imgNode.src = src; 
        }
    } 
})();
//代理负责预加载图片
var proxyImage = (function(){ 
    var img = new Image; 
    img.onload = function(){
        myImage.setSrc( this.src ); 
    }
    return {
        setSrc: function( src ){
             myImage.setSrc( 'file:// /C:/Users/svenzeng/Desktop/loading.gif' );
             img.src = src;  
        }
    } 
})();
proxyImage.setSrc('http://imgcache.qq.com/music/photo/k/000GGDys0yA0Nk.jpg' );

//虚拟代理实现合并http请求的案例
var synchronousFile = 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(){ //2秒后向本体发送需要同步的ID集合
            synchronousFile( cache.join( ',' ) ); 
            clearTimeout( timer ); // 清空定时器 
            timer = null;
            cache.length = 0; // 清空 ID 集合
        }, 2000 ); 
    }// 2 秒后向本体发送需要同步的 ID 集合
})();
//点击选中框,文件同步
var checkbox = document.getElementsByTagName( 'input' );
   for ( var i = 0, c; c = checkbox[ i++ ]; ){
      c.onclick = function(){
      if ( this.checked === true ){
          proxySynchronousFile( this.id ); }
      }
};

4.迭代器模式

定义:指提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示。迭代器模式可以把迭代的过程从业务逻辑中分离出来,在使用迭代器模式之后,即使不关心对象的内部构造,也可以按顺序访问其中的每个元素。


类型:

1.内部迭代器:内部定义好迭代规则,外部调用一次

2.外部迭代器:必须显示请求迭代下一个元素

3.倒序迭代器:倒序访问聚合对象

4.中止迭代器:像break一样,提供一种跳出循环的方法


js语言已经内置迭代器: Array.prototype.forEach


5.发布-订阅模式

定义:又叫观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。在 Js中,我们一般用事件模型来替代传统的发布—订阅模式。


作用:

1.广泛应用于异步编程中,替代传递回调函数。如订阅ajax的error、success事件等

2.可以取代对象之间硬编码的通知机制,一个对象不用再显示调用另一个对象的某个接口。


优点:时间上解耦,对象之间解耦

缺点:创建订阅者要消耗一定的时间和内存,而且订阅者会始终存在于内存中,无论订阅的消息有没有发生。

当多个发布者和订阅者嵌套在一起时,很难追踪bug产生来源。


注:不一定要先订阅后发布,也可以通过堆栈实现先发布后订阅


// 中介公司的案例
//全局事件对象
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 (let index = 0; index < fns.length; index++) {
            const fn = fns[index];
            fn.apply(this, arguments); // arguments是发布消息时附送的参数
        }
    };
  
    remove = function(key, fn) {
        var fns = clientList[key];
        if (!fns) { // 如果key对应的消息没有被人订阅,则直接返回
            return false;
        }
        if (!fn) { // 如果没有传入具体的回调函数,表示需要取消key对应消息的所有订阅
            fns && (fns.length = 0);
        } else {
            for (var l = fn.length - 1; l >= 0 ; l--) { // 反向遍历订阅的回调函数列表
                var _fn = fns[l];
                if (_fn === fn) {
                    fns.splice(l, 1); // 删除订阅者的回调函数
                }
            }
        }
    };
    return {
        listen,
        trigger,
        remove
    };
})();

Event.listen( 'squareMeter88', fn1 = function( price ){ // 小红订阅消息
     console.log( '价格= ' + price );  // 输出:'价格=200 0000'
});
Event.trigger( 'squareMeter88', 2000000 );// 售楼处发布消息
Event.remove( 'squareMeter88', fn1 );// 删除小红的订阅

6.命令模式

命令模式是最简单和优雅的模式之一,命令模式中的命令(command)指的是一个执行某些特定事情的指令


宏命令是一组命令的集合,通过执行宏命令的方式,可以一次执行一批命令。

撤销命令是命令模式中一个非常有用的功能,如Ctrl+Z的功能


Js中命令模式不一定要封装在 execute 方法中,也可以封装在普通函数中,和策略模式一样。

\

// 一键执行自动化的案例
var closeDoorCommand = { 
    execute: function(){  // 执行命令
        console.log( '关门' ); 
    },
    undo: function(){   // 撤销命令
        console.log( '开门' ); 
    }
};
var openPcCommand = { 
    execute: function(){
        console.log( '开电脑' ); 
    },
    undo: function(){
        console.log( '关电脑' ); 
    }
};
var openQQCommand = { 
    execute: function(){
        console.log( '登录 QQ' ); 
    },
    undo: function(){
        console.log( '退出 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(); 
              }
           },
           undo: function(){
              for ( var i = 0, command; command = this.commandsList[ i++ ]; ){
                  command.undo(); 
              }
           },
      
      }
};
var macroCommand = MacroCommand();

macroCommand.add( closeDoorCommand );  
macroCommand.add( openPcCommand );
macroCommand.add( openQQCommand );

macroCommand.execute(); //一键执行
macroCommand.undo(); //一键撤销

参考:

  1. [JavaScript设计模式与开发实践]