前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >设计模式之单例模式

设计模式之单例模式

作者头像
一粒小麦
发布2019-10-23 19:30:09
5790
发布2019-10-23 19:30:09
举报
文章被收录于专栏:一Li小麦一Li小麦一Li小麦

单例模式是创建对象最简单的方式。单例模式的定义 是:保证一个类仅有一个实例,并提供一个访问它的全局访问点。

在JavaScript开发中,单例模式的用途同样非常广泛。试想,当我们单击登录按钮的时候,页面中会出现一个登录浮窗,而这个登录浮窗是唯一的,无论单击多少次登录按钮,这个浮窗都只会被创建一次,那么这个登录浮窗就适合用单例模式来创建。

创建单例

实现单例模式的要点是:用flag量来表示是否为某类创建过对象,下次调用的时候,如该对象已经创建,就返回已经创建的对象实例,否则创建一个对象实例并返回。

class Person{
    constructor(name){
        this.name=name;
        this.instance=null;
    }

    getName(){
        console.log(this.name);
    }
}

Person.getInstance=function(name){
    this.instance=!this.instance?new Person(name):this.instance;
    return this.instance;
}

let a=Person.getInstance('djtao');
let b=Person.getInstance('dangjingtao');

console.log(a===b) // true

在上面的例子中,Person类实例由始自终只有一个。保证了它的独一无二。

但代码存在一个问题,就是"不透明",使用者必须通过通过 getInstance调用之.

更加透明

说到对象,基本都用person为例子。现在来看点实用的。

比如,我要在页面中创建一个唯一的div节点。同样也可以使用 new关键字。

const CreateDiv=(function(){
    let instance;
    class CreateDiv{
        constructor(html){
            if(instance){
                return instance;
            }
            this.html=html;
            this.init();
            return instance=this;
        }
        init(){
            let div=document.createElement('div');
            div.innerHTML=this.html;
            document.body.appendChild(div);
        }
    }

    return CreateDiv;
})();

const a=new CreateDiv('djtao');
const b=new CreateDiv('dangjingtao');
console.log(a===b);//true

此时页面创建了只有一个新div且只显示 dangjingtao

在这段看似炫技的代码中,解决了不透明的问题。但又带来了新的问题。

为了把instance封装,上述采用匿名函数自执行和闭包。并且用使此函数返回了真正的构造函数。增加了复杂度,读起来也不舒服。

CreateDiv实际上做了两件事情,一个是创建对象,一个是保证只有一个对象。从单一指责原则来说,这不是一个好的做法。假如项目后期我不再需要一个单例,而需要用它来创造N个div,那就痛苦了。

单一职责原则是设计模式的重要原则: 应该有且仅有一个原因引起类的变更(There should never be more than one reason for a class to change) 单一职责原则为我们提供了一个编写程序的准则,要求我们在编写类,抽象类,接口时,要使其功能职责单一纯碎,将导致其变更的因素缩减到最少。 如果一个类承担的职责过多,就等于把这些职责耦合在一起。一个职责的变化可能会影响或损坏其他职责的功能。而且职责越多,这个类变化的几率就会越大,类的稳定性就会越低。 在软件开发中,经常会遇到一个功能类T负责两个不同的职责:职责P1,职责P2。现因需求变更需要更改职责P1来满足新的业务需求,当我们实现完成后,发现因更改职责P1竟导致原本能够正常运行的职责P2发生故障。而修复职责P2又不得不更改职责P1的逻辑,这便是因为功能类T的职责不够单一,职责P1与职责P2耦合在一起导致的。

使用代理

设想有这样一个工厂方法,能把普通的类转化成单例:

class CreateDiv{
    constructor(html){
        this.html=html;
        this.init();
    }
    init(){
        let div=document.createElement('div');
        div.innerHTML=this.html;
        document.body.appendChild(div);
    }
}

const Proxy=(function(){
    let instance=null;
    return function(html){
        if(!instance){
            instance=new CreateDiv(html)
        }
        return instance;
    }
})();
const a=new Proxy('dangjingtao);
const b=new Proxy('djtao');
console.log(a===b);//true

有了Proxy,CreateDiv就变成了一个简单的类。显然比上一段易读易用。

全局可访问:JavaScript的单例模式

上面代码都是基于类创建的单例。JavaScript并非是一个真正有"类"的语言。在实践中,有时并不需要做这种脱裤子放屁的事。(笔者注:本人还是比较喜欢做这种事。)

全局变量就是一个单例。js中声明全局变量还是非常简单的。比如在jquery中时 $。然而全局变量是js最广受诟病的缺点之一。如何避免?

命名空间

用属性来取代全局变量,比如用 a.b来取代 b。还可以动态地创建:

let App={};
App.nameSpace=function(name){
  const parts=name.split('.');
  let current=App;
  for(let attr in parts){
    if(!current[parts[attr]]){
      current[parts[attr]]={};
    }
  }
}
App.nameSpace('event');
App.nameSpace('dom.style');

这段代码等效于:

var App={
  event:{},
  dom:{
    style:{}
  }
}
闭包

类似jQuery之类的库,用的就是闭包来规避全局变量的问题。它把所有的代码装在一个自执行的函数中。只暴露一些和外界通讯的接口。

const user=(function(){
  const _name='djtao';
     const _job='programer';
  return {
    getUserInfo:function(){
      return `${_name}-${_job}`;
    }
  }
});

懒惰的单例

惰性是单例模式的重点。有用的程度超过了想象。

使用bootstrap做modal模态框时。我们总是会在html页面全局写段又臭又长的弹框:

<!-- Modal -->
<div class="modal fade" id="myModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel">
  <div class="modal-dialog" role="document">
    <div class="modal-content">
      <div class="modal-header">
        <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
        <h4 class="modal-title" id="myModalLabel">Modal title</h4>
      </div>
      <div class="modal-body">
        ...
      </div>
      <div class="modal-footer">
        <button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
        <button type="button" class="btn btn-primary">Save changes</button>
      </div>
    </div>
  </div>
</div>

正常状态下,一个按钮对应且只能对应一个模态弹框。

但我使用的时候,也许只是做其它操作,压根不想去点这玩意。也就是说,这段DOM代码被浪费了。

在设计代码时,应该考虑:用一个变量来判断是否创建过。你可以找寻这个dom节点。如果找不到,就创建。于是你可能会这么写:

const createModal=(function(){
  let modal;
  return function(){
    if(!modal){
      modal=document.createElement('div');
      modal.innerHTML=`...`;
      modal.style.display='none';
      document.body.appendChild(modal);
      return modal;
    }
  }
});

这是一个可用的惰性单例。但仍然是违反单一职责原则的。假设你辛苦做完项目后,嬗变的需求经理哪天又跟你说:我不要modal了,全部改为iframe。那你就得把相同的逻辑再copy一遍。把创建modal改为创建iframe。此刻你的心里想必是骂妈卖批(mmp)的吧。

设想,惰性单例无非就是这么一个逻辑,用伪代码表述就是:

声明对象obj
if(obj存在){
    obj=xxx
}

这段逻辑是可以从业务上抽离出来的。以下便是本文的精华:

const getSingle=function(fn){
  let result;
  return function(){
    return result||(result=fn.apply(this,arguments));
  }
}

有了这个方法,随便你怎么创建多少个方法。都是惰性的单例了。

// 业务代码
const createIframe=function(){
  let modal = document.createElement('div');
  modal.innerHTML = `...`;
  modal.style.display = 'none';
  document.body.appendChild(modal);
  return modal;
}

const createMMP=function(){
  // ...
}

const iframe=getSingle(createIframe);
const mmp=getSingle(createMMP);

当有人说起单例模式能干嘛时,如果你的反应是"获取对象",那其实是片面的。现在再看一个例子。

假如你在设计一个前端框架,遇到常见的场景:当ajax请求渲染一个列表(对应方法是render),你给它们每个item绑定click事件(bindEvent)。应该怎么做?

那就痛苦了吧!

当然解决思路不止一种。单例模式可以很优雅解决该问题:

const bindEvent=getSingle(function(){
  xxx.onclick=function(){
    // todo
  };
  return true;
});

const render=function(){
  // 渲染数据
  bindEvent();
}

render();
render();
render();

无论你怎么执行,最后只绑定了一次。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-10-20,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 一Li小麦 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 创建单例
  • 更加透明
  • 使用代理
  • 全局可访问:JavaScript的单例模式
    • 命名空间
      • 闭包
      • 懒惰的单例
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档