JavaScript 设计模式详解

简介

让系统代码可重用、可扩展、可解耦、更容易被人理解且保证代码可靠性。设计模式使代码真正工程化。

《JavaScript 设计模式详解》 image

设计原则:

  1. 开闭原则: 对扩展开放,对修改关闭

  2. 里氏转换原则: 子类继承父类,单独完全可以运行

  3. 依赖倒转原则: 引用一个对象,如果这个对象有底层类型,直接引用底层类型

  4. 接口隔离原则: 每一个接口应该是一种角色

  5. 合成/聚合复用原则: 新的对象应使用一些已有的对象,使之成为新对象的一部分

  6. 迪米特原则: 一个对象应对其他对象有尽可能少的了解

单例模式

概念

保证一个类只有一个实例,实现方法是先判断实例存在与否,如果存在直接返回,如果不存在就创建了再返回,确保一个类只有一个实例对象。

在 JavaScript 中,单例作为一个命名空间提供者,从全局命名空间里提供一个唯一的访问点来访问该对象。

var Singleton = (function () {
    var instantiated;
    function init() {
        /*这里定义单例代码*/
        return {
            publicMethod: function () {
                console.log('hello world');
            },
            publicProperty: 'test'
        };
    }
    return {
        getInstance: function () {
            if (!instantiated) {     //确保只有一个实例
                instantiated = init();  //使用init方法,是使publicMethod和publicProperty只在要使用的时候才初始化;
            }
            return instantiated;
        }
    };
})();
/*调用公有的方法来获取实例:*/
Singleton.getInstance().publicMethod();  // hello world

作用和注意事项

模式作用:

  1. 模块间通信

  2. 系统中某个类的对象只能存在一个

  3. 保护自己的属性和方法

注意事项:

  1. 注意this的使用

  2. 闭包容易造成内存泄露,不需要的要赶快清除

  3. 注意new的成本。(继承)

实例

在网页上实现一个登陆弹框,无论我们点击多少次登陆按钮,界面上始终只会显示一个登陆弹框,无法再创建第二个。

源码下载     demo演示

(1)获取DOM对象

var $ = function(id) {
    return typeof id === 'string' ? document.getElementById(id) : id;
};

为了便于之后关于DOM的一些操作,我们这里利用函数式编程的原理将获取目标 id 的元素对象方法封装了一下,直接利用 $(id) 就可以获取。

(2)弹框构造函数

var Modal = function(id, html) {
    this.html = html;
    this.id = id;
    this.open = false;
};

这里我们声明了一个 Modal 作为弹框的构造函数,并且再其内部定义了公有属性 html、id 和 open。html 用来定义弹框内部的内容,id 用来给弹框定义 id 名称,open 用于判断弹框是否打开。

(3)open方法

Modal.prototype.create = function() {
    if (!this.open) {
        var modal = document.createElement('div');
        modal.innerHTML = this.html;
        modal.id = this.id;
        document.body.appendChild(modal);
        setTimeout(function() {
            modal.classList.add('show');
        }, 0);
        this.open = true;
    }
};

在 Modal 的原型链上定义了 create 方法,方法内部我们创建并向 DOM 中插入弹框,同时给弹框加上一个 class 为 “show” 的动画效果。

(4)close方法

Modal.prototype.delete = function() {
    if (this.open) {
        var modal = $(this.id);
        modal.classList.add('hide');
        setTimeout(function() {
            document.body.removeChild(modal);
        }, 200);
        this.open = false;
    }
};

定义了 open 方法后我们这里定义关闭弹框的方法,在其内部给弹框对象添加 hide 类动画效果,最后在页面上移除弹框对象。

(5)创建实例

var createIntance = (function() {
    var instance;
    return function() {
        return instance || (instance = new Modal('modal', '这是一个弹框'))
    }
})();

这是实现单例模式的重要部分:

  1. 使用闭包封装了 instance 私有变量并返回一个函数
  2. 利用 || 语法判断如果 instance 不存在则执行后者的实例化 Modal 方法,存在则直接返回 instance,确保了只存在一个弹框实例

(6)按钮操作

var operate = {
    setModal: null,
    open: function() {
        this.setModal = createIntance();
        this.setModal.create();
    },
    delete: function() {
        this.setModal ? this.setModal.delete() : '';
    }
};

这里我们将按钮操作放在 operate 对象里,使得打开和关闭操作可以通过this获取实例setModal。

(7)绑定事件

$('open').onclick = function() {
    operate.open();
};
$('delete').onclick = function() {
    operate.delete();
};

最后我们将打开和删除方法绑定到两个按钮上去,至此我们用单例模式实现的弹框demo就实现了。

构造函数模式

概念

构造函数用于创建特定类型的对象——不仅声明了使用过的对象,构造函数还可以接受参数以便第一次创建对象的时候设置对象的成员值。你可以自定义自己的构造函数,然后在里面声明自定义类型对象的属性或方法。

作用和注意事项

模式作用:

  1. 用于创建特定类型的对象

  2. 第一次声明的时候给对象赋值

  3. 自己声明构造函数,赋予属性和方法

注意事项:

  1. 声明函数的时候处理业务逻辑

  2. 区分和单例的区别,配合单例实现初始化

  3. 构造函数大写字母开头

  4. 注意 new 的成本 (继承)

实例

强制使用new

function Person(name, age, job) {
    if (!(this instanceof Person)) {
        return new Person(name, age, job);
    }
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = function() {
        return this.name + 'is' +  this.age + 'years old';
    }
}
var person1 = new Person("Davis", 22, "student");
var person2 = Person("Faker", 21, "player");
console.log(person1.sayName());  // Davis is 22 years old
console.log(person2.sayName());  // Faker is 21 years old

建造者模式

概念

建造者模式可以将一个复杂的对象的构建与其表示相分离,使同样的构建过程可以创建不同的表示。如果我们用了建造者模式,那么用户就需要指定需要建造的类型就可以得到它们,而具体建造的过程和细节就不需要知道了。建造者模式实际就是一个指挥者,一个建造者,一个使用指挥者调用具体建造者工作得出结果的客户。主要用于“分步骤构建一个复杂的对象”。

作用和注意事项

模式作用:

  1. 分步创建一个复杂的对象

  2. 解耦封装过程和具体创建组件

  3. 无需关心组件如何组装

注意事项:

  1. 一定要一个稳定的算法进行支持(“分步骤”是一个稳定的算法)

  2. 加工工艺是暴露的

实例

一个土豪需要建一个别墅,然后直接找包工头,包工头再找工人把别墅建好。这里土豪不用直接一个一个工人的去找。只需包工头知道土豪需求,然后去找工人,工人干活,土豪也不需要知道房子具体怎么建,最后能拿到房就可以了。

//1.产出东西是房子
//2.包工头调用工人进行开工而且他要很清楚工人们具体的某一个大项
//3.工人是盖房子的 工人可以建厨房、卧室、建客厅
//4.包工头只是一个接口而已 他不干活 他只对外说我能建房子
function House() {
    this.kitchen = "";
    this.bedroom = "";
    this.livingroom = "";
}
function Contractor() {
    this.construct = function(worker) {
        worker.construct_kitchen();
        worker.construct_bedroom();
        worker.construct_livingroom();
    }
}
function Worker() {
    this.construct_kitchen =function() {
        console.log("厨房建好了");
    }
    this.construct_bedroom = function() {
        console.log("卧室建好了");
    }
    this.construct_livingroom = function() {
         console.log("客厅建好了");
    }
    this.submit = function() {
        var _house = new House();
        _house.kitchen = "finished";
        _house.bedroom = "finished";
        _house.livingroom = "finished";
        return _house;
    }
};
var worker = new Worker();
var contractor = new Contractor();
contractor.construct(worker);
// 主人要房子
var myhouse = worker.submit();
console.log(myhouse);

工厂模式

概念

工厂模式定义一个用于创建对象的接口,这个接口由子类决定实例化哪一个类。该模式使一个类的实例化延迟到了子类。而子类可以重写接口方法以便创建的时候指定自己的对象类型(抽象工厂)。

作用和注意事项

模式作用:

  1. 对象构建十分复杂

  2. 需要依赖具体的环境创建不同的实例

  3. 处理大量具有相同属性的小对象

注意事项:

1、不能滥用工厂,有时候仅仅是给代码增加复杂度

实例

简单工厂模式

var XMLHttpFactory = function() {};
XMLHttpFactory.createXMLHttp = function() {
    var XMLHttp = null;
    // XMLHttpFactory.createXMLHttp()这个方法根据当前环境的具体情况返回一个XHR对象
    if (window.XMLHttpRequest) {
        XMLHttp = new XMLHttpRequest();
    } else if (window.ActiveXObject) {
        XMLHttp = new ActiveXObject("Microsoft.XMLHTTP")
    }
    return XMLHttp;
};
var AjaxHander = function() {
    var XMLHttp = XMLHttpFactory.createXMLHttp();
    /*...具体操作... */
}

抽象工厂模式

var XMLHttpFactory = function() {};
XMLHttpFactory.prototype = {
// 如果真的要调用这个方法会抛出一个错误,它不能被实例化,只能用来派生子类
    createFactory:function() {
        throw new Error("This is an abstract class");
    }
}
var XHRHandler = function() {
    XMLHttpFactory.call(this);
};
XHRHandler.prototype = new XMLHttpFactory();
XHRHandler.prototype.constructor = XHRHandler;  // 重新定义 createFactory 方法
XHRHandler.prototype.createFactory = function() {
    var XMLHttp = null;
    if (window.XMLHttpRequest) {
        XMLHttp = new XMLHttpRequest();
    } else if (window.ActiveXObject) {
        XMLHttp = new ActiveXObject("Microsoft.XMLHTTP")
    }
    return XMLHttp;
}
var AjaxHander = function() {
    var XMLHttp = XMLHttpFactory.createXMLHttp();
    /*...具体操作... */
}

抽象工厂只留一个接口,不做具体的事,让别的子类来继承我这个类,然后做它自己的事。

代理模式

概念

代理模式(Proxy),为其他对象提供一种代理以控制对这个对象的访问。代理模式使得代理对象控制具体对象的引用。代理几乎可以是任何对象:文件,资源,内存中的对象,或者是一些难以复制的东西。

作用和注意事项

模式作用:

  1. 远程代理(一个对象将不同空间的对象进行局部代理)

  2. 虚拟代理(根据需要创建开销很大的对象,如图片预加载)

  3. 安全代理(控制真实对象的访问权限)

  4. 智能指引(调用对象代理处理另外一些事情,如垃圾回收机制)

注意事项:

  1. 不能滥用代理,有时候仅仅是给代码增加复杂度

实例

// 代理模式需要三方
// 1.买家
function buyer() {
    this.name = "Davis";
}
// 2.中介
function agent() {
}
agent.prototype.sell = function() {
    new seller(new buyer()).sell("50万")
}
// 3.卖家  收钱
function seller(buyer) {
    this.buyer_name = buyer.name;
    this.sell = function(money) {
        console.log("收到了来自" +  this.buyer_name + money + "人民币");  // 收到了来自Davis50万人民币
    }
}
(new agent).sell();

虚拟代理实现图片预加载

// 图片加载函数
var myImage = (function() {
  var imgNode = document.createElement("img");
  document.body.appendChild(imgNode);
  return {   //  提供一个对外的setSrc接口,外界调用这个接口,可以给该img标签设置src属性
    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:/loading.png");
      img.src = src;
    }
  }
})();
// 调用代理对象加载图片
proxyImage.setSrc("http://images/water.jpg");

命令模式

概念

将请求封装成对象,分离命令接受者和发起者之间的耦合。命令执行之前在执行对象中传入接受者,主要目的相互之间的解耦。简单而言分为三个对象:

  1. 发起者:发出调用命令即可,具体如何执行,谁执行并不需要清楚。

  2. 接受者:有对应的接口处理不同的命令,至于命令是什么,谁发出的,不用关心。

  3. 命令对象:上面讲发起者和接受者分开了,二者之间需要个连接桥梁。这就是命令对象。命令对象接受发送者的调用,然后调用接受者的相应接口。

作用和注意事项

模式作用:

  1. 将函数的封装、请求、调用结合为一体

  2. 调用具体的函数解耦命令对象与接收对象

  3. 提高程序模块化的灵活性

注意事项:

  1. 不需要借口一致,直接调用函数即可,以免造成浪费

实例

场景如下:有个按钮button,点击之后,调用menu对象的refresh方法,输出“刷新”的内容。

// 发送者
var setCommond = function(button, fn) {
    button.onClick = function() {
        fn()
    }
};
// 执行命令者
var menu = {
    reFresh: function() {
        console.log("刷新");
    },
    add: function() {
        console.log("增加");
    },
    delete: function() {
        console.log("删除");
    }
};
// 命令对象
var commondObj = function(reciver) {
    return function() {
        reciver.reFresh();
    }
};
var commondObj1 = commondObj(menu);
setCommond(btn1, commondObj1);

发送者(setCommond):不关心给哪个button,以及绑定什么事件,只要通过参数传入就好。

命令对象(commondObj):只需要接收到接受者的参数,当发送者发出命令时,执行就好。

接受者(menu):不用关心在哪里被调用被谁调用,只需要按需执行就好了。

策略模式

概念

定义一系列方法,封装起来使他们可以相互替换。也就是将策略封装在策略类中,当发起请求时管理类将请求委托给对应策略类。

  1. 发起者:发出调用命令即可,具体如何执行,谁执行并不需要清楚。

  2. 接受者:有对应的接口处理不同的命令,至于命令是什么,谁发出的,不用关心。

  3. 命令对象:上面讲发起者和接受者分开了,二者之间需要个连接桥梁。这就是命令对象。命令对象接受发送者的调用,然后调用接受者的相应接口。

作用

模式作用:

  1. 策略模式利用组合,委托等技术和思想,有效的避免很多if条件语句。

  2. 策略模式提供了开放-封闭原则,使代码更容易理解和扩展。

  3. 策略模式中的代码可以复用。

实例

假设我们现在有这样一个需求:需要根据form表单元素的不同类型,当点击的时候输出对应的表单元素类型。

 /**
  * 针对不同情况的策略算法封装在策略类fucs中,
  * 从调用事件中去除繁琐的if或者switch逻辑判断。达到解耦的目的
  * 加入后面再增加‘select’的选项增加对应的方法即可
  */
 var funcs = {
   text: function() {
     console.log('this is text')
   },
   radio: function() {
     console.log('this is radio')
   },
   checkbox: function() {
     console.log('this is checkbox')
   },
   default: function() {
     console.log('this is default')
   }
 }
 var renderDom = function(type) {
     /**
      * 只需要根据不同的入参,自行匹配策略类中的接口即可。
      */
     return (funcs[type] || funcs['default'])()
 }
renderDom('checkbox')
  1. 策略对象就是funcs对象,里面的不同属性接口对应的方法就是策略。与逻辑判断分离开,如果有不同的情况的出现,对应的增加属性接口即可。

  2. renderDom方法就是对应的管理类,只需要根据不同的type,去调用funcs不同的方法就ok了。如果type没有对应的接口,那就调用默认的default对应接口。

  3. 调用事件,就保持不变。将tyoe类型传过去就好了。

职责链模式

概念

职责链由多个不同的对象组成,发送者是发送请求的对象,而接收者则是链中那些接收请求并且对其进行处理或传递的对象。请求本身有时候也可以是一个对象,它封装了和操作有关的所有数据,基本实现流程如下:

  1. 发送者知道链中的第一个接收者,它向这个接收者发送该请求。

  2. 每一个接收者都对请求进行分析,然后要么处理它,要么它往下传递。

  3. 每一个接收者知道其他的对象只有一个,即它在链中的下家(successor)。

  4. 如果没有任何接收者处理请求,那么请求会从链中离开。

实例

现有A、B、C、D四个生产线,生产总量为2100,每个生产线的生产量未知且在0~2100(包括0和2100,保证总量为2100即可),生产完毕后,分6次进行运输,规定每台车辆只能运350,且装满后才能够出发,装车顺序为ABCD,即上一个生产线生产的东西装完后才能够装下一个生产线的东西,要求根据每条生产线的生产量,给出每台车辆的托运安排。

例如: A:100 , B:1400 ,C:500 , D:100
托运安排:
第一辆车: A:100 , B:250
第二辆车: A:0 , B:350
第三辆车: A:0 , B:350
第四辆车: A:0 , B:350
第五辆车: A:0 , B:100 ,C:250
第六辆车: A:0 , B:0 , C:250 ,D:100

class WearHouse{
        constructor(volume,wearHouse){
            this.volume=volume;
            this.wearHouse=wearHouse;
            this.outString=[];
        }
        next(take){
            if(this.wearHouse!=undefined)
            {
                this.wearHouse.takeOut(take).forEach((subT) => {
                    this.outString.push(subT)
                })
            }
        }
        takeOut(num){
            if(num>this.volume){
                num-=this.volume;
                var temp=this.volume;
                this.volume=0;
                this.outString.push(this.print(0,temp));
                this.next(num);
            }else{
                this.volume-=num;
                this.outString.push(this.print(this.volume,num));
            }
            var temOut=this.outString;
            this.outString=[];
            return temOut;
        }
        print(volumn,takeNum){}
    }
    class WearHouseA extends WearHouse{
        print(volumn,takeNum){
            return ['A',takeNum];
        }
    }
    class WearHouseB extends WearHouse{
        print(volumn,takeNum){
            return ['B',takeNum];
        }
    }
    class WearHouseC extends WearHouse{
        print(volumn,takeNum){
            return ['C',takeNum];
        }
    }
    class WearHouseD extends WearHouse{
        print(volumn,takeNum){
            return ['D',takeNum];
        }
    }
    var D=new WearHouseD(100,undefined);
    var C=new WearHouseC(500,D);
    var B=new WearHouseB(1400,C);
    var A=new WearHouseA(100,B);
    var strSet=[];
    for(var a=0;a<6;a++){
        strSet.push(...A.takeOut(350));
    }
    console.log(strSet);

结束语

使用设计模式的是为了提高我们解决问题的效率,不同的设计模式也是针对不同环境的特定方案,不仅仅是单独的某一种设计模式,大多数情况下都是多个模式共存的。切勿为了使用设计模式而强行引入,增加代码复杂度。

学习资源:链接:pan.baidu.com/s/1i5BYiGH 密码:3s4v

    原文作者:算法小白
    原文地址: https://juejin.im/entry/59995da46fb9a024a17efa68
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞