前端常用的8种设计模式

1.引入

简介:

设计模式代表了最佳的实践,通常被有经验的面向对象的软件开发人员所采用。设计模式是软件开发人员在软件开发过程中面临的一般问题的解决方案。

设计模式是一套被反复使用的、多数人知晓的、经过分类编目的、代码设计经验的总结。

使用设计模式是为了重用代码、让代码更容易被他人理解、保证代码可靠性。

毫无疑问,设计模式于己于他人于系统都是多赢的,设计模式使代码编制真正工程化,设计模式是软件工程的基石,如同大厦的一块块砖石一样。项目中合理地运用设计模式可以完美地解决很多问题,每种模式在现实中都有相应的原理来与之对应,每种模式都描述了一个在我们周围不断重复发生的问题,以及该问题的核心解决方案,这也是设计模式能被广泛应用的原因。

设计模式构成:

  1. 模式名称:模式的名字
  2. 环境和问题:描述在什么环境下出现什么特定的问题
  3. 解决方案:描述如何解决方案
  4. 效果:描述应用该模式后的效果,以及可能带来的问题

注意:设计模式的种类很多,各个模式都有它对应的场景,不能武断地认为某个模式就是最优的解决方案

设计模式对开发的意义:

通过学习这些设计模式,让你找到“封闭变换”,“低耦合”,“针对接口编程”,从而设计出易维护、易复用、扩展性好、灵活性高的程序

通过学习设计模式让你领悟面向对象编程的思想(SOLID:单一功能、开闭原则、里氏替换、接口隔离、依赖反转),到最后就可以抛弃设计模式,把这些思想应用在你的代码中,写出高内聚、低耦合、可扩展、易维护的代码了。此时已然是心中无设计模式,而处处是设计模式 了。

2.单例模式

确保一个类仅有一个实例,并且提供一个访问它的全局访问点。单例是一个用来划分命名空间并将一批相关的属性和方法组织在一起的对象,如果它可以被实例化,那么它只能被实例化一次。

优点:

  1. 可以来划分命名空间,从而清除全局变量所带来的危险。
  2. 利用分支技术来来封装浏览器之间的差异。
  3. 可以把代码组织的更为一体,便于阅读和维护

**缺点:**由于单例模式提供的是一种单例访问,所以它有可能导致模块间的强耦合

**使用场景:**浏览器的window对象就是一个单例,在JavaScript开发中,对于这种只需要一个的对象,我们的实现往往使用单例。

举个例子:

    function CreateSingle(name) {
        this.name = name;
    }
    CreateSingle.prototype.getName = function () {
        console.log(this.name);
    }

    var Singleton = (function () {
        var instance;
        return function (name) {
            if (instance) {
                return instance
            }
            instance=new CreateSingle(name)
            return instance
        }
    })()


    var a=new Singleton('a');
    var b=new Singleton('b');
	console.log(a===b);//true

3.装饰器模式

装饰器模式在不改变原类和继承的情况下动态扩展对象功能,通过包装一个对象来实现一个新的具有原对象相同接口的新的对象

装饰器模式是一种为对象增添特性的技术,它并不使用创建新子类这种手段。装饰器模式可以用来透明地把对象包装在具有同样接口的另一对象之中。这样一来,我们可以给一个方法添加一些行为,然后将方法调用传递给原始对象。

相对于创建子类来说,使用装饰器对象是一种更加灵活的选择。

优点::

  1. 不需要修改对象原本的结构来进行功能添加
  2. 装饰对象和原对象具有相同的接口,可以使客户以与原对象相同的方式使用装饰对象
  3. 装饰对象中包含原对象的引用,即装饰对象为真正的原对象在此包装的对象

缺点:

  1. 在遇到用装饰器包装起来的对象时,那些依赖于类型检查的代码会出问题。
  2. 使用装饰器模式往往会增加架构的复杂程度。

使用场景:

如果需要为类增添特性或职责,而从该类派生子类的解决办法并不实际的话,就应该使用装饰器模式。

例子:

  function Sale(price) {
        this.price=price
        this.decorators_list=[]
    }

    Sale.prototype.decorate=function(decorator){
        this.decorators_list.push(decorator);
    }

    // 装饰者对象
    Sale.decorators={};

    // 装饰者
    Sale.decorators.centraltax={
        getPrice:function(price){
            return price*1.05
        }
    }

    Sale.decorators.localtax={
        getPrice:function(price){
            return price*1.075
        }
    }

    Sale.decorators.payment={
        getPrice:function(price){
            return '$'+price.toFixed(2);
        }
    }
    // 装饰器的调用
    Sale.prototype.getPrice=function(decorate){
        var price=this.price
        var dLen=this.decorators_list.length
        for (let i = 0; i < dLen; i++) {
            var name=this.decorators_list[i]
            price=Sale.decorators[name].getPrice(price)            
        }
        return price
    }

    var good=new Sale(500);
    // 添加需要的装饰器到decorators_list中
    good.decorate('centraltax');
    good.decorate('localtax');
    good.decorate('payment');
    // 使用
    console.log(good.getPrice());

3.适配器模式

适配器模式(Adapter)是将一个类(对象)的接口(方法或属性)转化成客户希望的另外一个接口(方法或 属性),适配器模式使得原本由于接口不兼容而不能一起工作的那些类(对象)可以一起工作。

适配器模式的作用是解决两个软件实体间的接口不兼容的问题。使用适配器模式之后,原本由于接口不兼容而不能工作的两个软件实体可以一起工作。适配器的别名是包装器(wrapper),这是一个相对简单的模式。在程序开发中有许多这样的场景:当试图调用模块或者对象的某个接口时,却发现这个接口的格式并不符合目前的需求。这时候有两种解决办法,第一种是修改原来的接口实现,但如果原来的模块很复杂,或者拿到的模块是一段别人编写的经过压缩的代码,修改原接口就显得不太现实了。第二种办法是创建一个适配器,将原接口转换为客户希望的另一个接口,客户只需要和适配器打交道。

优点:

适配器有助于避免大规模改写现有客户的代码。其工作机制是用一个新的接口对现有的类的接口进行包装。

缺点:

如果现有API还未 定形,新接口还未定形,那么适配器可能不会一直管用。

  var googleMap={
        show:function(){
            console.log('谷歌地图');
        }
    }
    var baiduMap={
        display:function(){
            console.log('百度地图');
        }
    }

    var baiduMapAdapter={
        show:function(){
            return baiduMap.display()
        }
    }
    var renderMap=function(map){
        if (map.show instanceof Function) {
            map.show()
        }
    }

    renderMap(googleMap);
    renderMap(baiduMapAdapter)

4.观察者模式(发布订阅模式)

观察者模式又叫做发布订阅模式,它定义了一种一对多的关系,让多个观察者对象同时监听一个主题对象,这个主题对象的状态发生变化时就会通知所有的观察者对象,使得他们能够自动更新自己。

观察者模式有观察者和被观察者构成。

调用顺序:

1.准备阶段(维护目标和观察者关系的阶段):

(1)创建目标对象

(2)创建观察者

(3)向目标对象注册观察者对象

2.运行阶段:

(1)改变目标对象的状态

(2)通知所有注册的观察者对象进行想应的处理

(3)调用目标对象,获取想应的数据

优点:

  1. 观察者模式实现了观察者和目标之间的抽象耦合

    原生目标对象在状态发生改变的时候,需要直接调用所有的观察者对象,但是抽象出观察者接口后,目标和观察者就只是在抽象层面耦合了,也就是说目标只知道观察者接口,并不知道具体的观察者的类,从而实现目标类和具体的观察者类之间的解耦

  2. 观察者模式实现了动态联动

    所谓联动,就是做一个操作会引起其他相关的操作。由于观察者模式对观察者注册实现管理,那就可以在运行 期间,通过动态地控制注册的观察者,来控制某个动作的联动范围,从而实现动态联动

  3. 观察者模式支持广播通信

    由于目标发送通知给观察者是面向所有注册的观察者,所以每次目标通知的信息就要对所有注册的观察者进行 广播。当然,也可以通过在目标上添加新的功能来限制广播的范围。

缺点: 可能会引起无谓的操作

实例:

有一个猎人工会,每个人都有发布任务和订阅任务的功能

每个人都有一个订阅列表来显示谁订阅了自己的任务

 // 创建也给猎人类
    function Hunter(name, level) {
        this.name = name;
        this.level = level
        this.list = []//订阅列表
    }

    // 发布任务
    Hunter.prototype.publish = function (money) {
        console.log(this.level + '猎人' + this.name + '寻求帮助');
        this.list.forEach(function (item, index) {
            item(money)
        })
    }

    // 订阅任务
    Hunter.prototype.subscribe = function (target, fn) {
        target.list.push(fn);
        console.log(this.level + '猎人' + this.name + '订阅了' + target.name);
    }


    // 创建猎人
    var hunterZhang = new Hunter('张师', '钻石');
    var hunterLi = new Hunter('李师', '星耀');
    var hunterWang = new Hunter('王师', '青铜');
    var hunterLiu = new Hunter('刘师', '黄金');


    // 老张订阅任务
    hunterZhang.subscribe(hunterWang, function (money) {
        console.log('张师表示' + (money > 400 ? 'OK!!' : '我很忙,你的出家太low'));
    })
    hunterLi.subscribe(hunterWang, function (money) {
        console.log('李师表示:我帮你');
    })
    hunterLiu.subscribe(hunterWang, function (money) {
        console.log('刘师表示:是兄弟就来砍我');
    })

    // 老王现在发布一个任务,来寻求帮助
    hunterWang.publish(500);

结果:

在这里插入图片描述

上面的这种写法时传统的观察者模式,当时现在的发布订阅模式都是会采用中间件的形式进行,发布和订阅的操作不会直接联系,而是通过第三方的中介进行联系。
在这里插入图片描述

可以参考一下这篇文章:

https://zhuanlan.zhihu.com/p/51357583

关于发布订阅模式我们也有一个实例:

定义一家猎人工会
主要功能包括任务发布大厅(topics),以及订阅任务(subscribe),发布任务(publish)

 // 任务发布大厅(中间件)
    var HunterUnion = {
        type: 'hunt',
        topics:Object.create(null),
        subscribe: function (topic, fn) {
            if (!this.topics[topic]) {
                this.topics[topic] = []
            }
            this.topics[topic].push(fn);
        },
        publish: function (topic, money) {
            if (!this.topics[topic]) {
                return
            }
            for (var  fn of this.topics[topic]) {
                fn(money)
            }
        }
    }

    // 定义一个猎人类
    function Hunter(name, level) {
        this.name = name
        this.level = level
    }
    // 订阅任务
    Hunter.prototype.subscribe = function (topic, fn) {
        console.log(this.level + '猎人' + this.name + '订阅了狩猎' + topic + '的任务');
        HunterUnion.subscribe(topic, fn);
    }
    // 发布任务
    Hunter.prototype.publish = function (topic, money) {
        console.log(this.level + '猎人' + this.name + '发布了狩猎' + topic + '的任务');
        HunterUnion.publish(topic, money);
    }

    // 创建猎人
    var hunterMing = new Hunter('小明', '黄金')
    var hunterJin = new Hunter('小金', '白银')
    var hunterZhang = new Hunter('小张', '黄金')
    var hunterPeter = new Hunter('Peter', '青铜')

    // 实例对象分别有他们的任务
    hunterMing.subscribe('tiger', function (money) {
        console.log('小明表示:' + (money > 200 ? '' : '不') + '接取任务')
    })
    hunterJin.subscribe('tiger', function (money) {
        console.log('小金表示:接取任务')
    })
    hunterZhang.subscribe('tiger', function (money) {
        console.log('小张表示:接取任务')
    })
    //Peter订阅了狩猎sheep的任务
    hunterPeter.subscribe('sheep', function (money) {
        console.log('Peter表示:接取任务')
    })
    // peter发布任务
    hunterPeter.publish('tiger',500);

5.策略模式

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

策略模式指的是定义一系列的算法,并且把它们封装起来,但是策略模式不仅仅只封装算法,我们还可以对用 来封装一系列的业务规则,只要这些业务规则目标一致,我们就可以使用策略模式来封装它们;

我们这里举一个例子

比如公司的年终奖是根据员工的工资和绩效来考核的,绩效为A的人,年终奖为工资的4倍, 绩效为B的人,年终奖为工资的3倍,绩效为C的人,年终奖为工资的2倍;

使用普通的模式编码的话,我们一般会:

var calculateBouns = function(salary,level) {
    if(level === 'A') {
return salary * 4;
   }
    if(level === 'B') {
return salary * 3;
   }
    if(level === 'C') {
return salary * 2;
   }
};
console.log(calculateBouns(1000,'A'))

这样的作法会造成以下:

  1. calculateBouns函数包含过多的if-else语句
  2. calculateBouns 函数缺乏弹性,假如还有D等级的话,那么我们需要在calculateBouns 函数内添加判断等 级D的if语句
  3. 算法复用性差,如果在其他的地方也有类似这样的算法的话,但是规则不一样,我们这些代码不能通用。

若我们将以上的形式改写为策略模式:

  var obj={
        'A':function(salary){
            return salary*4
        },
        'B':function(salary){
            return salary*3
        },
        'C':function(salary){
            return salary*2
        }
    }
    obj.D=function(){
        return '无该等级'
    }

    var caculateBouns=function(salary,level){
        return obj[level](salary)
    }

    console.log(caculateBouns('400','D'));
    console.log(caculateBouns('400','A'));

策略模式的优点:

  1. 策略模式利用组合,委托等技术和思想,有效的避免很多if条件语句。
  2. 策略模式提供了开放-封闭原则,使代码更容易理解和扩展。
  3. 策略模式中的代码可以复用。

6.模板模式

介绍:

模板模式又叫做模板方法,它定义了一个操作中的算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。

模板方法使一种代码复用的基本技术,在类库尤为重要,因为他们提取了类库中的公共行为。

实例:

需求:泡茶和泡咖啡。

​ 泡茶 泡咖啡
​ 1 烧开水 烧开水
​ 2 浸泡茶叶 冲泡咖啡
​ 3 倒入杯子 倒入杯子
​ 4 加柠檬 加糖

  var Drink = function () { };
    Drink.prototype.isNeed = function () {};
    Drink.prototype.firststep = function () {
        console.log('烧开水');
    }
    Drink.prototype.secondstep = function () {
    }
    Drink.prototype.thirdtstep = function () {
        console.log('倒入杯子');
    }
    Drink.prototype.fourthstep = function () {
    }
    Drink.prototype.init = function () {
        this.firststep();
        this.secondstep();
        this.thirdtstep();
        if (this.isNeed()) {
            this.fourthstep();
        }
    }
    // 泡茶类
    var Tea = function () { };
    Tea.prototype = new Drink();
    Tea.prototype.secondstep = function () {
        console.log('浸泡茶叶');
    }
    
    Tea.prototype.fourthstep = function () {
        console.log('加柠檬');
    }
    Tea.prototype.isNeed=function(){
        return confirm('您加柠檬吗?');
    }

    // 泡咖啡
    var Coffee = function () { };
    Coffee.prototype = new Drink();
    Coffee.prototype.secondstep = function () {
        console.log('冲泡咖啡');
    }
    Coffee.prototype.fourthstep = function () {
        console.log('加糖');
    }
    Coffee.prototype.isNeed=function(){
        return confirm('您加糖吗?');
    }

    // 实例化
    var tea = new Tea();
    tea.init();
    var coffee = new Coffee();
    coffee.init();

模板方法应用于下列情况:

  1. 一次性实现一个算法的不变的部分,并将可变的行为留给子类来实现
  2. 各子类中公共的行为应被提取出来并集中到一个公共父类中以避免代码重复,不同之处分离为新的操作,分别实现
  3. 控制子类扩展,模板方法可以只在特定点调用“Hook”操作,这样就只允许在这些点进行扩展。

注意:

钩子(Hook)操作,它提供了缺省的行为,子类在必要时进行扩展。一个钩子操作的缺省操作通常是一个空 操作。在模板方法中应指明哪些操是钩子操作(可以被重定义)以及哪些是抽象操作(必须被重定义)。

7.代理模式

代理模式(Proxy),为其他对象提供一种代理以控制对这个对象的访问

代理模式使得代理对象控制具体对象的引用。代理几乎可以是任何对象:文件,资源,内存中的对象,或者是一些难以复制的东西

一个实例:

假如“Sasan”要送“美女”玫瑰花,却不知道她的联系方式或者不好意思,想委托 她“闺蜜”去送这些玫瑰,那“闺蜜”就是个代理

   	//女孩类
    var Girl=function(name){
        this.name=name
    }
    //男孩类
    var Boy=function(grilname){
        this.grilname=grilname
        this.sendGift=function(gift){
            console.log('你好:'+grilname+'同学,这是大壮送你的礼物'+gift);
        }
    }
    //代理类
    var Proxy=function(grilname){
        this.grilname=grilname
        this.sendGift=function(gift){
            (new Boy(this.grilname)).sendGift(gift)
        }
    }
    var gg=new Girl('红红');
    var proxy=new Proxy(gg.name)
    proxy.sendGift('键盘');

使用场景:

  1. 远程代理,也就是为了一个对象在不同的地址空间提供局部代表,这样可以隐藏一个对象存在于不同地址空间的事实,就像web service里的代理类一样。
  2. 虚拟代理,根据需要创建开销很大的对象,通过它来存放实例化需要很长时间的真实对象,比如浏览器的渲 染的时候先显示问题,而图片可以慢慢显示
  3. 安全代理,用来控制真实对象访问时的权限,一般用于对象应该有不同的访问权限。
  4. 智能指引,只当调用真实的对象时,代理处理另外一些事情。

8.外观模式

外观模式(Facade)为子系统中的一组接口提供了一个一致的界面,此模块定义了一个高层接口,这个接口 值得这一子系统更加容易使用。

外观模式不仅简化类中的接口,而且对接口与调用者也进行了解耦。外观模式经常被认为开发者必备,它可以 将一些复杂操作封装起来,并创建一个简单的接口用于调用。

外观模式经常被用于JavaScript类库里,通过它封装一些接口用于兼容多浏览器,外观模式可以让我们间接调 用子系统,从而避免因直接访问子系统而产生不必要的错误。

外观模式的优势是易于使用,而且本身也比较轻量级。但也有缺点,外观模式被开发者连续使用时会产生一定 的性能问题,因为在每次调用时都要检测功能的可用性。

实例(浏览器适配):

var addMyEvent = function (el, ev, fn) {
    if (el.addEventListener) {
        el.addEventListener(ev, fn, false);
   } else if (el.attachEvent) {
        el.attachEvent('on' + ev, fn);
   } else {
        el['on' + ev] = fn;
   }
}; 

9.面试点

1、什么是装饰器模式?装饰器模式的缺点是什么?在什么场景使用?

装饰器模式在不改变原类和继承的情况下动态扩展对象功能,通过包装一个对象来实现一个新的具有原对象相同接口的新的对象。

缺点:

1)在遇到用装饰器包装起来的对象时,那些依赖于类型检查的代码会出问题。
2)使用装饰器模式往往会增加架构的复杂程度。

使用场景:

1)如果需要为类增添特性或职责,而从该类派生子类的解决办法并不实际的话,就应该使用装饰器模式。
2)如果需要为对象增添特性而又不想改变使用该对象的代码的话,也可以采用装饰器模式。

2、简述一下装饰器模式与适配器模式的区别。

  1. 适配器模式模拟实现简单的装饰模式的功能,也就是为已有功能添加功能
  2. 一般适配器适配过后是需要改变接口的,如果不改变接口就没有必要适配了;而装饰模式是不改变接口的,无论多少层装饰都是一个接口。因此装饰模式可以很容易地支持递归组合,而适配器就做不到,每次的接口不同,无法递归。

3.什么是观察者模式?观察者模式在什么场景使用?

它定义了一种一对多的关系,让多个观察者对象同时监听某一个主题对象,这个主题对象的状态发生变化时就会通知所有的观察者对象,使得它们能够自动更新自己。观察者模式在 javascript 中使用非常广泛。所有的浏览器时间就是该模式的实现。
使用场景:

  1. 当一个抽象模型有两个方面,其中一个方面的操作依赖于另一个方面的状态变化,那么就可以选用观察者模式
  2. 如果在更改一个对象的时候,需要同时连带改变其他的对象,而且不知道究竟应该有多少对象需要被连带改变,这种情况可以选用观察者模式
  3. 当一个对象必须通知其他对象,但是你又希望这个对象和其他被它统治的对象是松散耦合的。这个对象相当于是目标对象,而被它通知的对象就是观察者对象了。

4.策略模式的优点?

  1. 策略模式利用组合,委托等技术和思想,有效的避免很多if条件语句。
  2. 策略模式提供了开放-封闭原则,使代码更容易理解和扩展。
  3. 策略模式中的代码可以复用。

4.模板模式的优点?

  1. 封装不变的部分,扩展可变的部分。
  2. 提取公共代码,便于维护。
  3. 行为由父类控制,子类实现。

5.何时使用外观模式呢?
一般用于浏览器的兼容处理等情况。

  • 9
    点赞
  • 72
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值