JS中的设计模式

118 阅读8分钟

所有设计模式的主旨都是将变化和不变的部分隔离开来

单例模式

  • 保证一个类仅有一个实例,并提供一个全局访问点,有一些对象我们往往只需要一个,比如线程池、全局缓存、浏览器中的 window 对象等
const createSingleton = function(name, age){
                    if(!createSingleton.instance){
                        this.name =name;
                        this.age = age;
                        createSingleton.instance = this;
                    }
                    return createSingleton.instance
        }
  • 使用代理实现单例模式,把负责管理单例的逻辑移到了代理类 proxySingletonCreateDiv 中。这样一来, CreateDiv 就变成了一个普通的类,它跟 proxySingletonCreateDiv 组合起来可以达到单例模式的效果
var ProxySingletonCreateDiv = (function(){
var instance;
return function( html ){
if ( !instance ){
instance = new CreateDiv( html );
}
return instance;
}
})();
var a = new ProxySingletonCreateDiv( 'sven1' );
var b = new ProxySingletonCreateDiv( 'sven2' );
alert ( a === b )
  • 惰性单例(触发条件时才创建单例),还是先将管理单例的逻辑抽离出来
var getSingle = function( fn ){
var result;
return function(){
return result || ( result = fn .apply(this, arguments ) );
}
};

然后用getSingle包裹一下真正创建东西的函数,返回最终作为回调触发的函数

策略模式

统一制定一系列策略,通过控制传参触发不同策略,有效避免多重条件选择语句,将算法封装在独立的 strategies 中,使得它们易于扩展复用

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

用策略模式实现表单验证

  1. 先统一制定一系列验证策略
const strategies = {
    isNonEmpty: function( value, errorMsg ){
        if ( value === '' ){
        return errorMsg;
        }
    },
    minLength: function( dom, length, errorMsg ){
        if ( dom.length < length ){
        return errorMsg;
        }
    },
    isMobile: function( value, errorMsg ){
        if ( !/(^1[3|5|8][0-9]{9}$)/.test( value ) ){
        return errorMsg;
        }
    }
};
  1. 定义Validator类
const Validator = function(){
    this.cache=[]
}
//rulesArr包含多个对象,每个对象代表一种限制策略,有strategy,errorMsg键值对
Validator.prototype.add = function(dom, rulesArr){
    const self = this
    for(let i=0;i<rulesArr.length;i++){
        var rule = rulesArr[i];
        var strategyAry = rule.strategy.split( ':' );
        var errorMsg = rule.errorMsg;
        //把每一个验证函数的触发函数存在cache中,在start方法里启动
        self.cache.push(function(){
            //取到策略的键名
            var strategy = strategyAry.shift();
            strategyAry.unshift( dom.value );
            strategyAry.push( errorMsg );
            return strategies[ strategy ].apply( dom, strategyAry );
    }
}
//启动用户自定义的一系列表单验证
Validator.prototype.start = function(){
    for ( let i = 0, i<this.cache.length;i++; ){
        const validatorFunc = this.cache[i];
        var errorMsg = validatorFunc();
        if ( errorMsg ){
        return errorMsg;
        }
    }
};
  1. 客户端调用方式
var validataFunc = function(){
    var validator = new Validator();
    validator.add( registerForm.userName, [{
        strategy: 'isNonEmpty',
        errorMsg: '用户名不能为空'
    }, {
        strategy: 'minLength:6',
        errorMsg: '用户名长度不能小于 10 位'
    }]);
    validator.add( registerForm.password, [{
        strategy: 'minLength:6',
        errorMsg: '密码长度不能小于 6 位'
    }]);
    validator.add( registerForm.phoneNumber, [{
        strategy: 'isMobile',
        errorMsg: '手机号码格式不正确'
    }]);
    var errorMsg = validator.start();
    return errorMsg;
}

registerForm.onsubmit = function(){
    var errorMsg = validataFunc();
    if ( errorMsg ){
        alert ( errorMsg );
        return false;
    }
}

发布订阅模式

class EventBus {
  constructor() {
    this.events = {}; // 存储事件和对应的订阅者回调
  }

  // 订阅
  subscribe(event, callback) {
    if (!this.events[event]) {
      this.events[event] = [];
    }
    this.events[event].push(callback);
    return () => this.unsubscribe(event, callback); // 返回取消订阅函数
  }

  // 取消订阅
  unsubscribe(event, callback) {
    if (!this.events[event]) return;
    this.events[event] = this.events[event].filter(cb => cb !== callback);
  }

  // 发布
  publish(event, data) {
    if (!this.events[event]) return;
    this.events[event].forEach(callback => callback(data));
  }
}

// 导出一个全局单例
export const pubsub = new EventBus();

代理模式

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

虚拟代理实现图片预加载

(预加载其实就是新建一个Image对象,附上src,然后onload...)

var myImage = (function(){
        var imgNode = document.createElement( 'img' );
        document.body.appendChild( imgNode );
        return {
        setSrc: function( src ){
        //在赋值src之前写好避免图片被cache住无法触发回调
        imgNode.onload...
        imgNode.onerror...
        imgNode.src = src;
    }
    }
    })();
var proxyImage = (function(){
//创建一个虚拟image对象,来代理对真正img节点的操作,通过 proxyImage 间接地访问 MyImage,执行放loading图,proxy onload后放真正图等操作
        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' )

虚拟代理合并网络请求

const syncFile = function(id){
               console.log(‘开始同步文件’ + id) 
}

const proxySyncFile = (function(){
                var timer = null, cache=[];
                return function(id){
                          cache.push(id)
                          if(timer){
                            return
                        }
                        // 代理函数收集2秒后调用真正的同步文件函数
                        timer = setTimeout(function(){
                        syncFile( cache.join( ',' ) ); 
                        clearTimeout( timer ); // 清空定时器
                        timer = null;
                        cache.length = 0; // 清空 ID 集合
                        }, 2000 );
                }
                
})()
//使用
var checkbox = document.getElementsByTagName( 'input' );
for ( var i = 0, c; c = checkbox[ i++ ]; ){
    c.onclick = function(){
    if ( this.checked === true ){
    proxySyncFile( this.id );
    }
}};

缓存代理节省计算资源

var bigCalc = function(){
console.log( '开始昂贵运算' );
var a = 1;
for ( let i = 0, l = arguments.length; i < l; i++ ){
a = a * arguments[i];
}
return a;
};
//缓存代理
const proxyCalc = (function(){
var cache = {};
return function(){
var args = Array.prototype.join.call( arguments, ',' );
if ( args in cache ){
return cache[ args ];
}
return cache[ args ] = bigCalc.apply( this, arguments );
}
})();

proxyCalc( 1, 2, 3, 4 ); // 输出: 24
proxyCalc( 1, 2, 3, 4 ); // 从缓存输出: 24
、、

工厂模式

/* 工厂类 */
class Factory {
    static makeProduct(type) {
        switch (type) {
            case 'Product1':
                return new Product1()
            case 'Product2':
                return new Product2()
            default:
                throw new Error('当前没有这个产品')
        }
    }
}

/* 产品类1 */
class Product1 {
    constructor() { this.type = 'Product1' }
    
    operate() { console.log(this.type) }
}

/* 产品类2 */
class Product2 {
    constructor() { this.type = 'Product2' }
    
    operate() { console.log(this.type) }
}

const prod1 = Factory.makeProduct('Product1')
prod1.operate()																	// 输出: Product1
const prod2 = Factory.makeProduct('Product3')		// 输出: Error 当前没有这个产品

装饰者模式

用面向切面编程的范式(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;
}
};

用法:

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)})
.before(function(){alert(2)})
var button = document.getElementById( 'button' );

document的getElementById方法是before返回的新函数:
function(){ beforefn.apply( this, arguments ); return __self.apply( this, arguments ); }
注意:这里的this指向的是调用before的对象,也就是原getElementById函数

应用实例

数据统计上报

showLogin = showLogin.after( log )

表单上报

formSubmit = formSubmit.before( validate )

命令模式

var MoveCommand = function( receiver, pos ){
this.receiver = receiver;
this.pos = pos;
this.oldPos = null;
};

MoveCommand.prototype.execute = function(){
this.oldPos = this.receiver.dom.getBoundingClientRect()[ this.receiver.propertyName ];
// 记录小球开始移动前的位置
this.receiver.start( 'left', this.pos, 1000, 'strongEaseOut' );
};

MoveCommand.prototype.undo = function(){
this.receiver.start( 'left', this.oldPos, 1000, 'strongEaseOut' );
// 回到小球移动前记录的位置
};
var moveCommand;

moveBtn.onclick = function(){
var animate = new Animate( ball );
moveCommand = new MoveCommand( animate, pos.value );
moveCommand.execute();
};
cancelBtn.onclick = function(){
moveCommand.undo(); // 撤销命令
};

享元模式

var Upload = function( uploadType){
    //上传类型属于内部状态,是每个文件上传单例不变的部分
    this.uploadType = uploadType;
};

Upload.prototype.delFile = function( id ){
    //给对应id的文件对象设置了外部状态后,可以获取到该文件的大小和dom节点等
    uploadManager.setExternalState( id, this ); // (1)
    if ( this.fileSize < 3000 ){
        return this.dom.parentNode.removeChild( this.dom );
}
    if ( window.confirm( '确定要删除该文件吗? ' + this.fileName ) ){
        return this.dom.parentNode.removeChild( this.dom );
}
};

定义一个工厂来创建 upload 对象,如果某种内部状态对应的共享对象已经被创建过, 那么直接返回这个对象,否则创建一个新的对象

var UploadFactory = (function(){
    var createdFlyWeightObjs = {};
    return {
        create: function( uploadType){
            if ( createdFlyWeightObjs [ uploadType] ){
            return createdFlyWeightObjs [ uploadType];
            }
            return createdFlyWeightObjs [ uploadType] = new Upload( uploadType);
            }
        }
})();

最后来书写uploadManager和启动上传函数:

var uploadManager = (function(){
    var uploadDatabase = {};
    return {
    //manager的add方法 => 通过工厂create方法 => 调用构造函数创建或直接返回已存在文件上传单例 => 更新DOM => 将文件名,大小等信息写入database
    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 );
            
            //享元的灵魂,不直接赋给对象,因为所有文件共享一个上传对象,名称大小会被重复赋值覆盖,存在database中,用的时候先把外部状态set进去再取
            uploadDatabase[ id ] = {
                fileName: fileName,
                fileSize: fileSize,
                dom: dom
            };
            return flyWeightObj ;
    },
    //manager的setExternalState方法 => 从database取出对应文件信息对象 => 把该对象所有属性赋给文件上传单例
    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 = uploadManager.add( ++id, uploadType, file.fileName, file.fileSize );
}
};

现在就算现在同时上传 2000 个文件,需要创建的 upload 对象数量依然是 2。

startUpload( 'plugin', [
{
fileName: '1.txt',
fileSize: 1000
},
{
fileName: '2.html',
fileSize: 3000
},
{
fileName: '3.txt',
fileSize: 5000
}
]);

对象池

对象池维护一个装载空闲对象的池子,如果需要对象的时候,不是直接 new,而是转从对象池里获取。如 果对象池里没有空闲对象,则创建一个新的对象,当获取出的对象完成它的职责之后, 再进入池子等待被下次获取

对象池技术的应用非常广泛, HTTP 连接池和数据库连接池都是其代表应用。在 Web 前端开发中,对象池使用最多的场景大概就是跟 DOM 有关的操作。很多空间和时间都消耗在了 DOM节点上,如何避免频繁地创建和删除 DOM 节点就成了一个有意义的话题

职责链模式

将任务一步步在节点向后传递,直到有节点能执行它

var order500 = function( orderType, pay, stock ){
    if ( orderType === 1 && pay === true ){
    console.log( '500 元定金预购,得到 100 优惠券' );
    }else{
    return 'nextSuccessor'; // 我不知道下一个节点是谁,反正把请求往后面传递
    }
};
var order200 = function( orderType, pay, stock ){
    if ( orderType === 2 && pay === true ){
    console.log( '200 元定金预购,得到 50 优惠券' );
    }else{
    return 'nextSuccessor'; // 我不知道下一个节点是谁,反正把请求往后面传递
}
};
var orderNormal = function( orderType, pay, stock ){
    if ( stock > 0 ){
    console.log( '普通购买,无优惠券' );
    }else{
    console.log( '手机库存不足' );
    }
};

//定义Chain类
Class Chain {
    constructor(fn){
        this.fn = fn;
        this.successor = null;
    }
    
    setSuccessor(fn){
        this.successor = fn
    }
    
    start(){
        const res = this.fn.apply(this, arguments);
        if(res === 'nextSuccessor'){
        return this.successor && this.successor.start.apply(this.successor,arguments);
        }
        
        return res;
    }
    
    //用来处理异步操作, 手动控制传给下一个节点的时机
    next(){
        return this.successor && this.successor.start.apply(this.successor,arguments);
    }
    
}

用法

var chainOrder500 = new Chain( order500 );
var chainOrder200 = new Chain( order200 );
var chainOrderNormal = new Chain( orderNormal );
然后指定节点在职责链中的顺序:
chainOrder500.setNextSuccessor( chainOrder200 );
chainOrder200.setNextSuccessor( chainOrderNormal );
最后把请求传递给第一个节点:
chainOrder500.start( 1, true, 500 ); // 输出: 500 元定金预购,得到 100 优惠券
chainOrder500.start( 2, true, 500 ); // 输出: 200 元定金预购,得到 50 优惠券
chainOrder500.start( 3, true, 500 ); // 输出:普通购买,无优惠券
chainOrder500.start( 1, false, 0 ); // 输出:手机库存不足

状态模式

var Light = function(){
    this.currState = FSM.off; // 设置当前状态
    this.button = null;
};
Light.prototype.init = function(){
    var button = document.createElement( 'button' ),
    self = this;
    button.innerHTML = '已关灯';
    this.button = document.body.appendChild( button );
    this.button.onclick = function(){
        self.currState.buttonWasPressed.call( self ); // 把请求委托给 FSM 状态机
        }
};
var FSM = {
off: {
    buttonWasPressed: function(){
        console.log( '关灯' );
        this.button.innerHTML = '下一次按我是开灯';
        this.currState = FSM.on;
        }
    },
on: {
    buttonWasPressed: function(){
        console.log( '开灯' );
        this.button.innerHTML = '下一次按我是关灯';
        this.currState = FSM.off;
        }
    }
}