常用的JavaScript设计模式

2018-12-07阅读(943)评论(0)fishCat

W3Cways.com 微信公众号

1,委托模式

通过将多个对象的统一格式请求委托给同一个对象来减少事件处理或者内存开销

示例代码:

/*一般方式*/
<ul>
<li onClick="change"></li>
<li onClick="change"></li>
<li onClick="change"></li>
<li onClick="change"></li>
</ul>

let change = function(){
    //do sth here
}

/*委托方式*/
<ul onClick="change">
<li></li>
<li></li>
<li></li>
<li></li>
</ul>

let change = function(event){
    //根据event判断如果是LI标签,则统一触发事件
    //do sth here
}

备注:委托不仅可以节省内存开销,也使动态的插入或者移除HTML元素时,无需重新绑定事件

2,观察者模式

通过定义一种对象和观察者之间的依赖关系,解决对象和观察者之间的强耦合

示例代码:

/*观察者模式*/
let personList = [];
let person = {
    name:"A",
    age:"18",
    sex:"male"
}

let personInstance = {
    addPerson:function(){
        //add person
        //personList.push(person);
        
        //触发观察者事件
        this.personChange();
    },
    deletePerson:function(){
        //delete person
    },
    updatePerson:function(){
        //update person
   },
    personChange:function(){
        //do sth here
        console.log("change")
    }

}

//或者
let A = {
    name:"张三",
    age:"18"
};
Object.defineProperties(A,
{
"name":{
        get: function () {
            console.log("getter")
            return "李四"
        },
        set : function (val) {
           console.log("setter")
        },
        configurable : false
    }
});


//高级一点
(function ($) {

    var o = $({});

    $.subscribe = function () {
        o.on.apply(o, arguments);
    };

    $.unsubscribe = function () {
        o.off.apply(o, arguments);
    };

    $.publish = function () {
        o.trigger.apply(o, arguments);
    };

} (jQuery));

备注:关于观察者模式和发布订阅模式一直有争执,其实它们应该可以算是一种模式,只是一个不关心谁发布谁订阅,只管触发回调,也无法阻止回调;另一个必须要维护一个调度中心来管理这些,可以控制回调的触发与否。

3,工厂模式

通过分类抽象出对象工厂,然后方便的生产出各种标准一致的‘产品’

示例代码:

//对象工厂
let person = {
    age:"",
    sex:"",
    name:"",
    income:"",
    address:"",
    idCard:""
}
/*工厂方法*/
let createStudent = function(options){
    return Object.assign(person,options)
}

/*生产学生*/
let studentA = createStudent({
    grade:"大四",
    schoolName:"中国科技大学"
})
//组装工厂
let jsonToQueryString = function(json){
        return '?' +
            Object.keys(json).map(function (key) {
                return encodeURIComponent(key) + '=' +
                    encodeURIComponent(json[key]);
            }).join('&');
}

let getStr = jsonToQueryString({
    a:"1",
    b:"2",
    c:{
        c1:"",
        c2:""
    }
})

//拆卸工厂等:略
备注:该模式可以节省重复劳动,节省代码量,使代码美观易读易维护,
并让各部分的逻辑参数等符合同样的标准,避免人工手写复制粘贴失误等

4,享元模式

通过共享细粒度的数据而节省内存和开销

//一般模式:

// 雇佣模特
let HireModel = function(sex,clothes){
  this.sex = sex;
  this.clothes = clothes;
};
  
HireModel.prototype.wearClothes = function(){
  console.log(this.sex + '试穿' + this.clothes);
};
/*******试穿**********/
for(let i=0;i<100;i++){
  let model = new HireModel('male','第'+i+'款男衣服');
  model.wearClothes();
}
for(let i=0;i<100;i++){
  let model = new HireModel('female','第'+i+'款女衣服');
  model.wearClothes();
}

//享元模式

//雇佣模特
var HireModel = function(sex){
  //内部状态是性别
  this.sex = sex;
};
HireModel.prototype.wearClothes = function(clothes){
  console.log(this.sex+"穿了"+clothes);
};

//工厂模式,负责造出男女两个模特
var ModelFactory = (function(){
  var cacheObj = {};
  return {
    create:function(sex){//单例模式
      //根据sex分组
      if(cacheObj[sex]){
        return cacheObj[sex];
      } else {
        cacheObj[sex] = new HireModel(sex);
        return cacheObj[sex];
      }
    }
  };
})();
//模特管理
var ModelManager = (function(){
  //容器存储:1.共享对象 2.外部状态
  var vessel = {};
  return {
    add:function(sex,clothes,id){
      //造出共享元素:模特
      var model = ModelFactory.create(sex);
      //以id为键存储所有状态
      vessel[id] = {
        model:model,
        clothes:clothes
      };
    },
    wear:function(){
      for(var key in vessel){
        //调用雇佣模特类中的穿衣服方法。
        vessel[key]['model'].wearClothes(vessel[key]['clothes']);
      }
    }
  };
})();


/*******通过运行时间测试性能**********/
for(var i=0;i<100;i++){
  ModelManager.add('male','第'+i+'款男衣服',i);
  ModelManager.add('female','第'+i+'款女衣服',i);
}
ModelManager.wear();    

由于享元模式需要区分外部状态和内部状态,使得应用程序在某种程度上来说更加复杂化了。
为了使对象可以共享,享元模式需要将享元对象的状态外部化,而读取外部状态使得运行时间变长。
使用场景:
对象的属性可以拆分
对象的变量是一部分可以变动另外

享元模式其实使用的非常广泛。只要对象处于反复创建的环境中,并且每个对象有部分属性是共通的那么我们就能使用享元模式。符合这种场景的比如 java 中 string 的常量池,就是享元模式。还有jDK中的类文件放在方法区也是享元模式的一种。在互联网应用中通常我们描述的叫XX池的东西都是使用的享元模式。

思考:如果书的类别有40种,而作者只有10个,那么挑选哪个属性作为内部状态呢?
当然是作者,因为这样只需要创建10个享元对象就行了。

思考:为何不干脆定义一个没有内部状态的享元对象得了,那样只有一个享元对象用于共享?
这样当然是可以的,实际上变得跟单例模式很像,唯一的区别就是多了对外部状态的注入。
实际上内部状态越少,要注入的外部状态自然越多,而且为了代码的复用性,会让内部状态尽可能多。

5,单例模式

唯一,避免重复创建
//示例代码:
var singleton = function(fn) {
    var instance;
    return function() {
        return instance || (instance = fn.apply(this, arguments));
    }
};
// 创建遮罩层
var createMask = function(){
    // 创建div元素
    var mask = document.createElement('div');
    // 设置样式
    mask.style.position = 'fixed';
    mask.style.top = '0';
    mask.style.right = '0';
    mask.style.bottom = '0';
    mask.style.left = '0';
    mask.style.opacity = '0.75';
    mask.style.backgroundColor = '#000';
    mask.style.display = 'none';
    mask.style.zIndex = '98';
    document.body.appendChild(mask);
    // 单击隐藏遮罩层
    mask.onclick = function(){
        this.style.display = 'none';
    }
    return mask;
};

// 创建登陆窗口
var createLogin = function() {
    // 创建div元素
    var login = document.createElement('div');
    // 设置样式
    login.style.position = 'fixed';
    login.style.top = '50%';
    login.style.left = '50%';
    login.style.zIndex = '100';
    login.style.display = 'none';
    login.style.padding = '50px 80px';
    login.style.backgroundColor = '#fff';
    login.style.border = '1px solid #ccc';
    login.style.borderRadius = '6px';

    login.innerHTML = 'login it';

    document.body.appendChild(login);

    return login;
};

document.getElementById('btn').onclick = function() {
    var oMask = singleton(createMask)();
    oMask.style.display = 'block';
    var oLogin = singleton(createLogin)();
    oLogin.style.display = 'block';
    var w = parseInt(oLogin.clientWidth);
    var h = parseInt(oLogin.clientHeight);
}
//另一种单例模式
let global = {};
let singleModel = function(obj){
    global = Object.assign(obj,global);
    return global
}

new singleModel({
    doSthA:function(){console.log("a")},
    doSthB:function(){console.log("b")}
});
    
new singleModel({
    doSthB:function(){console.log("b1")},
    doSthC:function(){console.log("c")}
})
//使用
global....

6,链式调用

链式模式的核心思想就是在调用完对象的方法以后可以回当前对象(this),然后继续调用下去
//示例代码:
let jQuery = {
    foundId:function(){console.log(1);return this},
    addArr:function(){console.log(2);return this},
    delArr:function(){console.log(3);return this},
}
//执行
jQuery.foundId().addArr().delArr()
备注:这只是一种最简单的调用方式,仅供参考,举一反三

7,策略模式

策略模式的 目的是使算法脱离于模块而独立管理,解除代码之间的耦合影响
//示例代码:
let util = {
    getAgeByBirthday: function (strBirthday) {
        try {
            let bDay = new Date(strBirthday.split("-").join("/"));
            let nDay = new Date();
            let nbDay = new Date(nDay.getFullYear(), bDay.getMonth(),   bDay.getDate());
            let age = nDay.getFullYear() - bDay.getFullYear();
            if (bDay.getTime() > nDay.getTime()) { return -1 }
            return nbDay.getTime() <= nDay.getTime() ? age : --age;
        } catch (e) {
            return -1
        }

    },
    jsonToQueryString:function(json){
        return '?' +
            Object.keys(json).map(function (key) {
                return encodeURIComponent(key) + '=' +
                    encodeURIComponent(json[key]);
            }).join('&');
    }====
}
//。。。
各种常见的工具库等,都是会使用这种模式,需要注意的是,该对象里面只有算法,不能牵扯各种业务逻辑

8,状态模式

解决程序中繁杂的分支判断语句问题,避免大量多层的if else 嵌套

//示例代码:

//一般写法:
if(a){}
else if(b){}
else if(c){}
else if(d){}
//状态管理例子
let obj = {
    a:function(){
    
    },
    b:function(){
    
    }
    //...
}

//使用
let type = a || b || c || d;
obj[type]();

obj[a]();
状态管理模式可以较方便的解除各状态之间的耦合,
使本来只能从上到下的上下文执行方式,
变成可以分为多个分支的执行方式,而且更方便复用。

9,桥接模式

通过将实现层(如绑定的事件)与抽象层(如页面UI逻辑)解除耦合,使两部分可以独立
桥接模式定义:将抽象部分与它的实现部分分离,使它们都可以独立地变化。
桥接模式主要有4个角色组成:
(1)抽象类
(2)扩充抽象类
(3)实现类接口
(4)具体实现类

根据javascript语言的特点,我们将其简化成2个角色:
    
(1)扩充抽象类
(2)具体实现类
//示例代码:
var each = function (arr, fn) {
    for (var i = 0; i < arr.length; i++) {
        var val = arr[i];
        if (fn.call(val, i, val, arr)) {
            return false;
        }
    }
}
var arr = [1, 2, 3, 4];
each(arr, function (i, v) {
    arr[i] = v * 2;
})
在这个例子中,我们通过each函数循环了arr数组.
别看这个例子很常见,但其中就包含了典型的桥接模式。
在这个例子中,抽象部分是each函数,也就是上面说的扩充抽象类,实现部分是fn,即具体实现类。
抽象部分和实现部分可以独立的进行变化。这个例子虽然简单,但就是一个典型的桥接模式的应用。

插件开发中的桥接模式
桥接模式的一个适用场景是组件开发。

我们平时开发组件为了适应不同场合,组件相应的会有许多不同维度的变化。桥接模式就可以应用于此,将其抽象与实现分离,使组件的扩展性更高。

假设我们要开发一个弹窗插件,弹窗有不同的类型:
普通消息提醒,错误提醒,

每一种提醒的展示方式还都不一样。这是一个典型的多维度变化的场景。首先我们定义两个类:普通消息弹窗和错误消息弹窗。

function MessageDialog(animation) {
    this.animation = animation;
}
MessageDialog.prototype.show = function () {
    this.animation.show();
}
function ErrorDialog(animation) {
    this.animation = animation;
}
ErrorDialog.prototype.show = function () {
    this.animation.show();
}

这两个类就是前面提到的抽象部分,也就是扩充抽象类,它们都包含一个成员animation。
两种弹窗通过show方法进行显示,但是显示的动画效果不同。我们定义两种显示的效果类如下:

function LinerAnimation() {
}
LinerAnimation.prototype.show = function () {
    console.log("it is liner");
}
function EaseAnimation() {
}
EaseAnimation.prototype.show = function () {
    console.log("it is ease");
}

这两个类就是具体实现类,它们实现具体的显示效果。那我们如何调用呢?

var message = new MessageDialog(new LinerAnimation());
message.show();
var error = new ErrorDialog(new EaseAnimation());
error.show();

如果我们要增加一种动画效果,可以再定义一种效果类,传入即可。

10,模版模式

通过定义一套操作算法骨架,使得子类可以不改变父类的算法结构的同时,重新定义算法中的某些实现步骤
//示例(伪)代码:
<ul bindScroll="this.scroll">
    <li bindClick="this.click"></li>
</ul>

let picker = function(options){
    this.click = function(){};
    this.scroll = function(){};
    let init = function(options){};
    init();
}

let datePicker = new picker({});
    datePicker.click = function(){
        //TODO sth special
    }

11,适配器模式

将一个类(对象)的接口(方法或属性)转化为另外一个接口或对象,用来避免过多的硬编码和兼容

适配器有4种角色:

1.目标抽象角色(Target):定义客户所期待的使用接口。(VGI接口)
2.源角色(Adaptee):需要被适配的接口。(HDMI接口)
3.适配器角色(Adapter):把源接口转换成符合要求的目标接口的设备。(HDMI-VGI转换器)
4.客户端(client):例子中指的VGI接口显示器。

实例
假设有两种充电接口MicroUSB和USBTypec

function ChargingCord(name) {
  var _name = name || '默认:无接口'
  this.work = function () {
    console.log('使用' + _name + '接口');
  }
  this.getName = function () {
    return _name;
  }
  this.check = function (target) {
    return _name == target.getName();
  }
}

function MicroUSB() {
  this.__proto__ = new ChargingCord('MicroUSB');
}

function USBTypec() {
  this.__proto__ = new ChargingCord('USBTypec');
}

有两种车分别有不同的充电接口

function Car(name, chargingCord) {
  var _name = name || '默认:车'
  var _chargingCord = chargingCord || new ChargingCord();
  this.getName = function () {
    return _name;
  };
  this.charge = function (target) {
    if (_chargingCord.check(target.getChargingCord())) {
      console.log(this.getName());
      _chargingCord.work();
      console.log('充电');
      target.charging();
    }
    else {
      console.log(this.getName()+"的"+_chargingCord.getName());
      console.log(target.getName()+"的"+target.getChargingCord().getName());
      console.log('接口不对无法充电');
    }
  }
}    
function Porsche911() {
  this.__proto__ = new Car('Porsche911', new USBTypec());
}    
function Porsche781() {
  this.__proto__ = new Car('Porsche781', new MicroUSB());
}

有两种手机有不同的接受充电的接口

function Phone(name, chargingCord) {
  var _name = name || '默认:手机'
  var _chargingCord = chargingCord || new ChargingCord();
  this.getChargingCord = function () {
    return _chargingCord;
  };
  this.getName = function () {
    return _name;
  };
  this.charging = function () {
    console.log(_name);
    _chargingCord.work();
    console.log('接收');
  }
}    
function IPhone() {
  this.__proto__ = new Phone('IPhone', new USBTypec());
}    
function MIPhone() {
  this.__proto__ = new Phone('MIPhone', new MicroUSB());
}

我们分别用辆车个两种手机充电

var porsche911 = new Porsche911();
var porsche781 = new Porsche781();    
var iPhone = new IPhone();
var miPhone = new MIPhone();    
console.log('-----------------------------------------');
porsche911.charge(iPhone);
console.log('-----------------------------------------');
porsche781.charge(miPhone);
console.log('-----------------------------------------');
porsche781.charge(iPhone);
console.log('-----------------------------------------');

结果


Porsche911
使用USBTypec接口
充电
IPhone
使用USBTypec接口
接收


Porsche781
使用MicroUSB接口
充电
MIPhone
使用MicroUSB接口
接收

Porsche781的MicroUSB
IPhone的USBTypec
接口不对无法充电

Porsche911的USBTypec
MIPhone的MicroUSB
接口不对无法充电


所以我们要创建适配器函数

function PhoneUSBTypecToMicroUSB(Phone) {
  var _USBTypec = new ChargingCord('USBTypec');
  var _MicroUSB = new ChargingCord('MicroUSB');
  if (_USBTypec.check(Phone.getChargingCord())) {
    Phone.charging = function () {
      console.log(this.getName());
      _USBTypec.work();
      console.log('转接');
      _MicroUSB.work();
      console.log('接收');
    }
    Phone.getChargingCord = function () {
      return _MicroUSB;
    };
    return Phone;
  }
  else {
    console.log('接口不对无法转换');
  }
}

function PhoneMicroUSBToUSBTypec(Phone) {
  var _USBTypec = new ChargingCord('USBTypec');
  var _MicroUSB = new ChargingCord('MicroUSB');
  if (_MicroUSB.check(Phone.getChargingCord())) {
    Phone.charging = function () {
      console.log(this.getName());
      _MicroUSB.work();
      console.log('转接');
      _USBTypec.work();
      console.log('接收');
    }
    Phone.getChargingCord = function () {
      return _USBTypec;
    };
    return Phone;
  }
  else {
    console.log('接口不对无法转换');
  }
}

function PhoneDeleteInterface(Phone){
  delete Phone.charging;
  delete Phone.getChargingCord;
  return Phone;
}
再来测试接口转换和充电情况
PhoneMicroUSBToUSBTypec(iPhone);
console.log('-----------------------------------------');
PhoneUSBTypecToMicroUSB(miPhone);
console.log('-----------------------------------------');
porsche781.charge(PhoneUSBTypecToMicroUSB(iPhone));
console.log('-----------------------------------------');
porsche911.charge(PhoneMicroUSBToUSBTypec(miPhone));
console.log('-----------------------------------------');
porsche781.charge(PhoneDeleteInterface(iPhone));
console.log('-----------------------------------------');
porsche911.charge(PhoneDeleteInterface(miPhone));

适配后结果

接口不对无法转换


接口不对无法转换


Porsche781
使用MicroUSB接口
充电
IPhone
使用USBTypec接口
转接
使用MicroUSB接口
接收


Porsche911
使用USBTypec接口
充电
MIPhone
使用MicroUSB接口
转接
使用USBTypec接口
接收

Porsche781的MicroUSB
IPhone的USBTypec
接口不对无法充电

Porsche911的USBTypec
MIPhone的MicroUSB
接口不对无法充电

适配器模式优点
1.可以让任何两个没有关联的类一起运行。
2.提高了类的复用。
3.增加了类的透明度。
4.灵活性好。

适用场景
1.系统需要使用现有的类,而此类的接口不符合系统的需要。
2.想要建立一个可以重复使用的类,用于与一些彼此之间没有太大关联的一些类,包括一些可能在将来引进的类一起工作,这些源类不一定有一致的接口。
3.通过接口转换,将一个类插入另一个类系中。

12,节流模式

通过对重复执行的的业务逻辑进行控制,在执行完最后一次符合某条件的操作后,取消操作,并清理内存
//示例代码:
let timer= 0;
let _st = setInterval(function(){
    //do sth repeated
    let pass = checkPass();
    if(timer>=1000 || pass){
        clearInterval(_st)
    }
},1000);

let checkPass = function(){
    //TODO sth
    return true || false
};

应该是最简单的节流模式了orz

13,惰性模式

减少每次代码执行时的重复性的分支判断,通过对对象重新定义来屏蔽原对象中的分支判断。
即如果代码已经走过第一个分支逻辑,那么接下来,就没必要再让代码去执行各个分支的判断,
直接执行第一个逻辑即可
//示例代码:
let AddEvent = function(dom, type, fn){
  if(dom.addEventListener){
    AddEvent = function(dom, type, fn){
        dom.addEventListener(type, fn, false);
      }
  }else if(dom.attachEvent){
    AddEvent = function(dom, type, fn){
        dom.attachEvent('on'+type, fn);
      }
  }else{
    AddEvent = function(dom, type, fn){
        dom['on'+type] = fn;
      }
  }
 AddEvent(dom, type, fn);
};

转载请注明来源:Web前端(W3Cways.com) - Web前端学习之路 » 常用的JavaScript设计模式
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!