被问到设计模式不要怂:一文读懂单例模式
何为单例
顾名思义,就是一个类只有一个实例,并可以全局访问。比如说一个系统只有一个登录弹窗入口,这个登录弹窗就适合用单例模式设计。
如何实现
怎么在创建一个实例的时候知道,这个类在之前有没有实例?
1. flag
function A(name){
this.name=name
}
A.instance=null
A.getInstance=function(name){
if(!this.instance){
this.instance=new A(name)
}
return this.instance
}
2. 闭包
将上例 getInstance
改写:
A.getInstance=(function(name){
var instance=null
return function(name){
if(!instance){
instance=new A(nanme)
}
return instance
}
})()
前面两个虽能解决单例的实现,但我每次如果要想创建单例,就必须调用 getInstance
函数才可以,直接 new
毫无作用,这样写不是很龟毛吗?
3. 闭包+同名覆盖
将上例稍微改动一下,就可直接 new
了
var A=(function(){ // 注意用var
var instance=null
var A=function(){
if(instance) return instance;
this.init()
return instance=this
}
A.prototype.init=function(){
// do something
}
})()
这种方法虽然很妙,但可复用性不强,因为 A
是有两个职责的:一是进行初始化,二是单例的判断。
4. 使用代理类
顾名思义,代理类就是进行代理,把主要职责转发。这里我们把A写为普通类。
var A=function(name){
this.name=name
this.init()
}
A.prototype.init=function(){
// do something
}
// 创建代理类
var P=(function(){
var instance=null
return function(name){
if(!instance){
instance=new A(name)
}
return instance
}
})()
const p1=new P('aaa')
const p2=new P('bbb')
alert(p1===p2) // true
在JS中的应用
由于社区更新迭代速度非常快,尤其是前端工程化出现的各种工具、框架和思想,让我们在开发的时候,对于变量的控制方面的困扰也少了很多,但我们不能仅仅做一个框架的使用者而仅仅局限于眼前的操作而已,要知道框架对于某些功能的实现也是基于底层语言的,所以搞清这些东西是非常有必要的。
我们都知道,js是一门 class-free
语言,他是没有“类”这个概念的,es6引入的 class
也只是语法糖而已,我们按照上面使用“类”的思想构造单例,相当于脱裤放屁,本身我们要做单例,只是需要一个“唯一”的对象,并且可以被全局访问到而已,而在js中创建一个对象十分容易,关于对象是否可被全局访问,当然是使用全局变量,而全局变量作为js一个经常被人诟病的东西,要如何处理使其污染最小呢?
1.命名空间
使用命名空间可以极大地减少全局变量,而且它创建起来十分简单。
const namespace={
a:function(){},
b:{
c:()=>{}
}
}
但如果是作为一个 immutable
这远远不够,因为即使是用 const
声明的对象类型,也会被不小心修改,所以就不得不将对象冻结,但棘手的是,这个对象中的原始类型是修改且扩展不了了,但对于此对象中嵌套的对象类型的属性,仍然可以被修改,如下:
const namespace={
a:function(){},
b:{
c:()=>{}
}
}
Object.freeze(namespace)
namespace.d='ddddd' // 不能扩展
namespace.b='bbbbbb' // 不能修改属性
namespace.b.c='cccccc' // 但内嵌的对象可以修改属性和扩展
console.log(namespace)
/* 输出
a: ƒ ()
b: {c: "cccccc"}
*/
那这里就不得不去将属性b进行处理,如下:
Object.freeze(namespace)
Object.defineProperty(namespace.b,'c',{
writable:false,
configurable:false
})
namespace.d='ddddd' // 不能扩展
namespace.b='bbbbbb' // 不能修改属性
namespace.b.c='cccccc' // 内嵌的对象也不可被修改
console.log(namespace)
/* 输出
a: ƒ ()
b: {c: ƒ}
*/
但这里有个弊端就是,我必须知道他有对象类型的属性再进行封装,如果对象属性中还有对象属性就需要一层层去递归实现,让人头大。
2.使用闭包封装私有变量
把私有变量封装在闭包的内部,只暴露一些接口。
var namespace=(function(){
const _a=function(){}
const _b={
c:()=>{}
}
return {
getNameSpace:function(){
return ({
a:_a,
b:_b
})
}
}
})()
Object.freeze(namespace)
namespace.getNameSpace='1111'
console.log(namespace.getNameSpace())
/*输出:
a: ƒ ()
b: {c: ƒ}
*/
惰性单例
惰性单例是当只有需要的时候才会创建对象实例,就比如在第一节中我们实现的那个例子:
function A(name){
this.name=name
}
A.instance=null
A.getInstance=function(name){
if(!this.instance){
this.instance=new A(name)
}
return this.instance
}
但这是基于“类”的设计,上一节讲,在js中写这种单例模式等于脱裤放屁,那什么样的单例模式的实现才最有普适性呢?我将引入一个例子来分析。
在一个系统中,我们需要一个登录按钮,点击登录按钮需要弹出登录弹窗。
有两种设计思想可以实现,一是把弹窗组件写好先隐藏起来,之后通过改变css让其显示;二是当用户点击登录按钮时再去创建弹窗。因为本节讨论的是惰性单例,所以前者的实现不再讨论范围之内,所以我们来实现一下后者。
const createModal=function(){
let modal=null
return function(){
if(!modal){
modal=document.createElement('div')
modal.innerHTML= `login modal`
modal.style.display='none'
document.body.appendChild(modal)
}
return modal
}
}()
document.getElementById('loginBtn').onclick=()=>{
const loginModal=createModal()
loginModal.style.display='block'
}
当我多次点击登录按钮的时候,login modal只会创建一次。
但 createModal
这个函数的职责还不够单一,要想让单例模式应用到很多地方,就要把这部分的逻辑抽出来。
const getSingle=function(fn){
var res=null
return function(){
return res||(res= fn.apply(this,arguments))
}
}
然后我们再实现那个需求:
const createLoginLayer=function(){
const modal=document.createElement('div')
modal.innerHTML= `login modal`
modal.style.display='none'
document.body.appendChild(modal)
return modal
}
document.getElementById('loginBtn').onclick=()=>{
const createSingleLoginModal=getSingle(createLoginLayer)
const loginModal=createSingleLoginModal()
loginModal.style.display='block'
}
如果还需要构造什么其他的单例组件:
const createA=function(){
// .....
}
const createSingleA=getSingle(createA)
createSingleA()
这样就符合单一原则啦~~
当然 getSingle()
也不仅仅局限于创建dom,比如给元素绑定事件等功能上也可以达到一样的效果。但!不要觉得好用节省开销就处处使用这个函数,因为闭包会占用内存,进行调试也不好操作,所以有必要才可以用,不可画蛇添足。
最近在看《js设计模式与开发实践》,本文是我对此书的一些概括与扩展。下一篇文章写策略模式~~