JavaScript-5大常见的设计模式

JavaScript-5大常见的设计模式

前言

设计模式有不少种, 但在前端中如何运用?在前端编码实践中, 常见的设计模式有哪些?前端

设计模式的定义

设计模式, 是在面向对象软件设计中针对特定问题的简洁&优雅的解决方案. 在不一样的编程语言中, 对设计模式的实现, 可能会有区别. 好比Java和JavaScript, 在Java这种静态编译型语言中, 没法动态地给已存在的对象添加职责, 因此通常经过包装类的方式来实现装饰者模式. 但在JavaScript中, 给对象动态添加职责是很简单的事, 这也就形成了JavaScript语言的装饰者模式再也不关注于给对象动态添加职责, 而是关注于给函数动态添加职责.ajax

前端经常使用的5大设计模式

  • 工厂模式
  • 单例模式
  • 代理模式
  • 策略模式
  • 观察者模式

工厂模式

工厂模式, 是用来建立对象的一种经常使用的设计模式, 不暴露建立对象的具体逻辑, 而是将逻辑封装在一个函数中. 即这个函数能够视为一个工厂, 工厂模式根据抽象程度的不一样能够分为:算法

  • 简单工厂
  • 工厂方法
  • 抽象工厂

下面主要来介绍下简单工厂和工厂就去在JavaScript中的运用的一些简单示例.编程

简单工厂

简单工厂,又称为静态工厂, 由一个工厂对象决策建立某一种产品对象类的实例, 主要用来建立同一类对象.设计模式

例如, 在实际项目开发中, 咱们可能须要根据用户权限来渲染不一样的页面, 高级权限的用户所拥有的页面有些是没法被低级权限的用户所查看. 所以咱们能够在不一样权限等级用户的构造函数中, 保存该用户可以看到的页面.浏览器

function UserFactory(role){
  function SuperAdmin(){
    this.name = 'superManager'
    this.viewPage = ['home', 'userManage', 'orderManage', 'appManage', 'permManage']
  }
  function Admin(){
    this.name = 'admin'
    this.viewPage = ['home', 'orderMange', 'appManage']
  }
  function NormalUser(){
    this.name = 'normalUser'
    this.viewPage = ['home', 'orderManage']
  }
  switch(role){
    case 'superAdmin':
      return new SuperAdmin()
      break
    case 'admin':
      return new Admin()
      break
    case 'user':
      return new NormalUser()
      break
    default:
      throw new Error('参数错误, 可选参数:superAdmin、admin、user')
  }
}

let superAdmin = UserFactory('superAdmin')
let admin = UserFactory('admin')
let normalUser = UserFactory('user')
复制代码

小结

在上面的示例中, UserFactory就是一个简单工厂, 在该函数中有3个构造函数, 分别对应不一样权限的用户, 当咱们调用工厂函数时, 只须要传递superAdmin, admin, user这3个可选参数, 便可获取一个对应的实例对象.缓存

  • 优势: 只须要一个正确的参数, 就能够获取到你所须要的对象, 而无需知道其余建立的具体细节.
  • 缺点: 在函数内包含了全部对象的建立逻辑(构造函数)和判断逻辑代码, 每增长一个新的构建函数, 还须要修改判断逻辑代码, 若上面的对象是30个时或更多时, 这个函数会成为一个庞大的超级函数, 难以维护. 简单工厂只能做用于建立的对象数量较少,对象的建立逻辑不复杂时使用

工厂方法

工厂方法, 是将实际建立对象的工做推迟到子类中, 这样核心类就变成了抽象类, 可是在JavaScript中, 很难像传统面向对象那样去建立抽象类, 所以在JavaScript中咱们只要参考它的核心思想便可, 咱们能够将工厂方法看做是一个实例化对象的工厂类.安全

好比, 上面的例子, 咱们使用工厂方法来改造下. 工厂方法, 咱们只把它看做是一个实例化对象的工厂, 它只作实例化对象这一件事情, 咱们采用安全模式建立对象.服务器

function UserFactory(role){
  if(this instanceof UserFactory){
    if(typeof this[role] !== 'function') throw new Error('参数错误, 可选参数:superAdmin、admin、user')
    const s = new this[role]();
    return s;
  }else {
    return new UserFactory(role)
  }
}
UserFactory.prototype = {
  SuperAdmin: function (){
    this.name = 'superManager'
    this.viewPage = ['home', 'userManage', 'orderManage', 'appManage', 'permManage']
  },
  Admin: function (){
    this.name = 'admin'
    this.viewPage = ['home', 'orderMange', 'appManage']
  },
  NormalUser: function (){
    this.name = 'normalUser'
    this.viewPage = ['home', 'orderManage']
  }
}

const superAdmin = UserFactory('SuperAdmin');
const admin = UserFactory('Admin') 
const normalUser = UserFactory('NormalUser')
const user = UserFactory('user')
复制代码

小结

在简单工厂中,若是咱们新增长一个用户类型,须要修改两处地方的代码:markdown

  • 增长新的用户构造函数
  • 在逻辑判断中增长对新的用户的判断

而在抽象工厂方法中,咱们只须要在UserFactory.prototype中添加就能够啦。

单例模式

单例模式, 保证一个类只有一个实例, 而且提供一个访问它的全局访问点

在一些特定的需求场景中, 咱们须要保证一个对象只需一个, 例如:

  • 线程池
  • 全局缓存
  • 浏览器中的Window对象
  • 登陆窗等

实现思路: 用一个变量标识当前是否已经为某个类建立过对象, 若是是, 则在下一次获取这个类的实例时, 直接返回以前建立的对象.

这样作的优势是:

  • 能够用来划分命名空间, 减小全局变量的数量
  • 能够被实例化, 且只实例化一次

下面咱们来看一个简单的示例, 在JavaScript中咱们可使用闭包来实现这种模式:

var cls = (function(){
  var instance;
  function getInstance(){
    if(instance === undefined){
      instance = new Construct()
    }
    return instance;
  }
  function Construct(){
    // ... 构造函数
  }
  return {
    getInstance: getInstance
  }
})()
复制代码

小结

在上面的代码中,咱们可使用cls.getInstance来获取到单例,而且每次调用均获取到同一个单例.

在咱们平时的开发中,咱们也常常会用到这种模式,好比当咱们单击登陆按钮的时候,页面中会出现一个登陆框,而这个浮窗是惟一的,不管单击多少次登陆按钮,这个浮窗只会被建立一次,所以这个登陆浮窗就适合用单例模式。

代理模式

代理模式是一种很是有意义的模式, 在生活中能够找到不少代理模式的场景.

好比明星都有经纪人做为代理. 若是想请明星来办一场 商业演出, 只能联系他的经纪人. 经纪人会把商业演出的细节和报酬都谈好以后, 再把合同交给明星签. 代理模式的关键是, 当客户不方便直接访问一个对象或者不知足须要的时候, 提供一个替身对象来控制对这个对象的访问, 客户实际 上访问的是替身对象. 替身对象对请求作出一些处理以后, 再把请求转交给本体对象.

虚拟代理实现图片预加载

在Web开发中, 图片预加载是一种经常使用的技术, 若是直接给某个img标签节点设置src属性, 因为图片过大或者网络不佳, 图片的位置 每每有段时间会是一片空白. 常见的作法是先用一张loading图片占位, 而后用异步的方式加载图片, 等图片加载好了再把它填充 到img节点里, 这种场景就很适合使用虚拟代理.

var myImage = (function(){
        var imgNode = document.createElement("img");
        document.body.appendChild(imgNode);

        return function(src){
            imgNode.src = src;
        }
    })();

    var proxyImage = (function(){
        var img = new Image;
        img.onload = function(){
            myImage(this.src);
        };
        return function(src){
            myImage("file:// /C:/Users/sven/Desktop/loading.gif");
            img.src = src;
        }
    })();

    proxyImage("http://img/cache/com/music/dklddsafla.jpg");

复制代码

虚拟代理合并HTTP请求

在Web开发中, 也许最大的开销就是网络请求. 假设咱们在作一个文件同步的功能, 当咱们选中一个checkbox的时候, 它对应的文件 就会被同步到另外一台备用服务器上.当咱们选中3个checkbox的时候,依次往服务器发送了3次同步文件的请求. 而点击并非一个很复 杂的操做, 一秒内点中4个checkbox并非什么难事, 如此频繁的网络请求将会带来至关大的开销. 解决方案是, 能够经过一个代理函数proxySynchronousFile来收集一段时间内的请求, 最后一次性发送给服务器.好比咱们等待2秒之 后才把这2秒以内须要同步的文件ID打包发给服务器, 若是不是对实时性要求很是高的系统, 2秒的延迟不会带来太大的反作用, 却能 大大减轻服务器的压力.代码以下:

var synchronousFile = function(id){
        console.log("开始同步文件, id为: " + id);
    };
    var proxySynchronousFile = (function(){
        var cache = [];
        var timer;
        return function(id){
            cache.push(id);
            if(timer){
                return;
            }
            timer = setTimeout(function(){
                synchronousFile(cache.join(","));
                clearTimeout(timer);
                timer = null;
                cache.length = 0;
            }, 2000);
        };
    })();

    var checkbox = document.getElementsByTagName("input");
    for(var i= 0, c; c=checkbox[i++];){
        c.onclick = function(){
            if(this.checked === true){
                proxySynchronousFile(this.id);
            }
        }
    }

复制代码

策略模式

策略模式, 指的是定义一些列的算法,把他们一个个封装起来,目的就是将算法的使用与算法的实现分离开来,避免多重判断条件,更具备扩展性。

举个例子,如今超市有活动,vip为5折,老客户3折,普通顾客没折,计算最后须要支付的金额,若是不使用策略模式,咱们的代码可能和下面同样:

function Price(personType, price) {
 //vip 5 折
 if (personType == 'vip') {
 return price * 0.5;
 } 
 else if (personType == 'old'){ //老客户 3 折
 return price * 0.3;
 } else {
 return price; //其余都全价
 }
}
复制代码

在上面的代码中,咱们须要不少个判断,若是有不少优惠,咱们又须要添加不少判断,这里已经违背了刚才说的设计模式的六大原则中的开闭原则了,若是使用策略模式,咱们的代码能够这样写:

class Customer{
    constructor(name,discount){
        this.name = name
        this.discount = discount
    }
    getPrice(price){
        return this.discount * price
    }
    hello(){
        throw new Error("方法未实现!")
    }
}

class VipCustomer extends Customer{
    constructor(username){
        super('vip', 0.5)
        this.username = username
    }
    hello(){
        return `尊敬的VIP用户: ${this.username} 你好!`
    }
}

class OldCustomer extends Customer{
    constructor(username){
        super('normal', 0.89)
        this.username = username
    }
    hello(){
        return `尊敬的老用户: ${this.username} 你好!`
    }
}


const vip = new VipCustomer('小平')
window.console.log(vip.hello())
window.console.log("vip客户 的结帐价为:", vip.getPrice(200))

const old = new OldCustomer('小军')
window.console.log(old.hello())
window.console.log("普通客户 的结帐价为:", old.getPrice(200))
复制代码

小结

总结:在上面的代码中,经过策略模式,使得客户的折扣与算法解藕,又使得修改跟扩展能独立的进行。

当咱们的代码中有不少个判断分支,每个条件分支都会引发该“类”的特定行为以不一样的方式做出改变,这个时候就可使用策略模式,能够改进咱们代码的质量,也更好的能够进行单元测试。

观察者模式

发布-订阅模式的通用实现

真实的例子 --- 网站登陆

假如咱们正在开发一个商城网站, 网站里有header头部, nav导航, 消息列表, 购物车等模块. 这几个模块的渲染有一个共同的前提 条件, 就是必须先用ajax异步请求获取用户的登陆信息. 至于ajax请求何时能成功返回用户信息, 这点没法肯定. 如今的情节 看起来像极了售楼处的例子, 小明不知道何时开发商的售楼手续可以成功办下来. 更重要的一点是, 咱们不知道除了header头部, nav导航, 消息列表, 购物车以外, 未来还有哪些模块须要使用这些用户信息. 若是 它们和用户信息模块产生了强耦合, 好比下面这样的形式:

login.succ(function(data){
        header.setAvatar(data.avatar);      // 设置header模块的头像
        nav.setAvatar(data.avatar);         // 设置导航模块的头像
        message.refresh();                  // 刷新消息列表
        cart.refresh();                     // 刷新购物车列表
    });
复制代码

这种耦合性会使程序变得僵硬, header模块不能随意再改变setAvatar的方法名, 它自身的名字也不能被改成header1, header2.这是 针对实现编程的典型例子, 针对具体实现编程是不被赞同的. 用发布-订阅模式重写以后, 对用户信息感兴趣的业务模块将自行订阅登陆成功的消息事件. 当登陆成功时, 登陆模块只须要发布登陆 成功的消息, 而业务方接受到消息以后, 就会开始进行各自的业务处理, 登陆模块并不关心业务方究竟要作什么, 也不想去了解它们 的内部细节. 改善后的代码下:

$.ajax("http://xxx.com?login", function(data){  // 登陆成功
        login.trigger("loginSucc", data);           // 发布登陆成功的消息
    });

    // 和模块监听登陆成功的消息
    var header = (function(){       // header模块
        login.listen("loginSucc", function(data){
            header.setAvatar(data.avatar);
        });
        return {
            setAvatar: function(data){
                console.log("设置header模块的头像!");
            }
        }
    })();

    var nav = (function(){          // nav模块
        login.listen("loginSucc", function(data){
           nav.setAvatar(data.avatar);
        });
        return {
            setAvatar: function(avatar){
                console.log("设置nav模块的头像!");
            }
        }
    })();

    var address = (function(){      // 收货地址模块
        login.listen("loginSucc", function(obj){
            address.refresh(obj);
        });
        return {
            refresh: function(avatar){
                console.log("刷新收货地址列表!");
            }
        }
    })();
复制代码

全局的发布-订阅对象

在程序中, 发布-订阅模式能够用一个全局的Event对象来实现, 订阅者不须要了解消息来自哪一个发布者, 发布者也不知道消息会推送 给哪些订阅者, Event做为一个相似"中介者"的角色, 把订阅者和发布者联系起来.见以下代码:

但这里咱们要留意另外一个问题, 模块之间若是用了太多的全局发布-订阅模式来通讯, 那么模块与模块之间的联系就被隐藏到 背后. 咱们最终会搞不清楚消息来自哪一个模块, 或者消息会流向哪些模块, 这又会给咱们的维护带来一些麻烦, 也许某个模块 的做用就是暴露一些接口给其余模块调用.

全局事件的命名冲突

咱们所了解的发布-订阅模式中, 都是订阅者先订阅一个消息, 随后才能接收到发布者发布的消息. 若是把顺序返过来, 发布者先发 布一条消息, 而在此以前并无对象来订阅它, 这条消息无疑将消失在宇宙中. 在某些状况下, 咱们须要先将这条消息保存下来, 等到有对象来订阅它的时候, 再从新把消息发布给订阅者. 就如同QQ中的离线消息 同样, 离线消息被保存在服务器中, 接收人下次登陆上线以后, 能够从新收到这条消息. 这种需救济在实际项目中是存在的, 好比在以前折商城网站中, 获取到用户信息以后才能渲染用户导航模块, 而获取用户信息的操做 是一个ajax异步请求. 当ajax请求成功返回以后会发布一个事件, 在此以前订阅了此事件的用户导航模块能够接收到这些用户信息. 但这只是理想的情况, 由于异步的缘由, 咱们不能保证ajax请求返回的时间, 有时它返回得比较快, 而此时用户导航模块的代码尚未 加载好(尚未订阅相应的事件), 特别是在用了一些模块化惰性加载的技术后, 这是极可能发生的事情. 也许咱们还须要一个 方案, 使得咱们的发布-订阅对象拥有先发布后订阅的能力. 为了知足这个需求, 咱们要创建一个存放离线事件的堆栈, 当事件发布的时候, 若是此时尚未订阅者来订阅这个事件, 咱们暂时把 发布事件的动做包裹在一个函数里, 这些包装函数将被存入堆栈中, 等到终于有对象来订阅此事件的时候, 咱们将遍历堆栈而且依次 执行这些包装函数, 也就是从新发布里面的事件.固然离线事件的生命周期只有一次, 就像QQ的未读信息只会被从新阅读一次, 因此 刚才的操做咱们只能进行一次.

相关文章
相关标签/搜索