设计模式 最近在写Vue插件,发现自己通过直观想法写的东西总是与别人的插件有一定的差距。我也明白别人的库中用了一种或多种设计模式,使代码结构更加恰当等等。之前就说要学习一下设计模式,被搁置到现在,现在买了《JavaScript设计模式与开发实践》,后面会陆续将自己的学习过程通过博客记录。
单例模式 定义 保证一个类仅有一个实例,并提供一个访问它的全局访问点。
描述 单例模式是一种常用的模式,有一些对象我们往往只需要一个,比如线程池、全局缓存、浏览器中的window
对象。
实现 传统单例模式 JavaScript语言原生设计的时候并没有设计类的概念,并且对象是可以直接通过字面量来新建对象,不一定需要通过类来实例化对象。所以在JavaScript中的单例模式与Java等面向对象语言稍有不同。下面通过传统的面向对象语言的方式来设计单例模式。
简单实现 要实现标准的单例模式,无非是要用一个变量来标志当前是否已经为某个类创建过对象,如果创建过,则在下一次创建的时候直接返回该对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 let Singleton = function (name ){ this .name = name } Singleton .instance = null Singleton .prototype .getName =function ( ){ alert (this .name ) } Singleton .getInstance = function (name ){ if (!instance){ this .instance = new Singleton (name) } return this .instance } let a = Singleton .getInstance ('name1' )let b = Singleton .getInstance ('name2' )
或者使用闭包将instance
标志放在闭包中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 let Singleton = function (name ){ this .name = name } Singleton .prototype .getName = function ( ){ alert (this .name ) } Singleton .getInstance = (function ( ){ let instance = null return function (name ){ if (!instance){ instance = new Singleton (name) } } return instance })()
通过这种方式,我们只能通过getInstance
方法来获得该对象。这样增加了该方法的不透明性,不能通过new
来创建对象,下面来编写一个头i摩纳哥的单例类。
透明的单例类 下面来实现一个透明类,来实现一个CreateDiv
单例类,他的作用是是负责在页面中创建唯一的div
节点,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 let CreateDiv =(function ( ){ let instance; let CreateDiv =function (html ){ if (instance){ return instance } this .html = html this .init () return instance = this } CreateDiv .prototype .init = function ( ){ let div = document .createElement ('div' ) div.innerHTML = this .html document .body .appendChild (div) } return CreateDiv })() let a = new CreateDiv ('name1' )let b = new CreateDiv ('name2' )a === b
这个构造方法虽然完成了一个透明的单例类,但是它同样有一些缺点,即违背了“单一职责原理 ”
单一职责原则 (SRP:Single responsibility principle)又称单一功能原则,面向对象五个基本原则(SOLID)之一。它规定一个类应该只有一个发生变化的原因。
用代理实现单例模式 为了实现单一职责原理 ,我们要引入代理类的方式,把负责管理单例的代码分离出去。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 var CreateDiv = function (html ){ this .html = html this .init () } CreateDiv .prototype .init = function ( ){ let div = document .createElement ('div' ) div.innerHTML = this .html document .body .appendChild (div) } let proxyCreateDiv = (function ( ){ let instance return function (fn, html ){ if (!instance){ instance = new fn (html) } return instance } })() let a = new ProxySingletonCreateDiv (CreateDiv , 'name1' )let b = new ProxySingletonCreateDiv (CreateDiv , 'name2' )a === b
上面的代码中,我们将业务代码与管理逻辑分开到两个类中,实现了单一职责原理 。
JavaScript中的单例模式 由于JavaScript是一门原生无类(class-free)语言,也是正因如此,生搬硬套单例模式的概念并无意义。
单例模式的核心是:确保只有一个实例,并提供全局访问。
全局变量不是单例模式,但在JavaScript中,我们经常会把全局变量当成单例来使用。
当我们使用字面量对象在全局作用域下创建对象a时,对象a确实是独一无二的,并且也是可以在任意位置上访问。这样满足单例模式的两个条件。
但是这样会污染全局变量,我们可以使用两种方法:
使用命名空间。
使用闭包封装私有变量。
惰性单例 所谓惰性单例就是在需要的时候才创建。实际上上面的单例模式就已经是惰性单例了,在为初始化之前都是null
,只有在new
的时候才进行创建。
下面用一个实际应用场景,就是登录框的设计:在用户未点击之前,登录框不会显示,点击时才会出现登录框。
这里有几种处理方式:
提前将登录框插入到文档中,并将其display
属性设置为none
,在点击时将display
属性设置为block
。这样性能问题就是,这个登录框可能永远用不到,这样就浪费了性能。
在用户点击时,使用JavaScript将登录加入文档流;在用户登录成功或者×
时,将其从文档流中删除。这样的性能问题也有,就是第一次生成的登陆框实际是可以保存,方便以后使用。
所以最佳方案就是最开始不生成登录框,等用户点击时生成,并且这之后将其display
属性设为none
,方便以后使用,这样也就是我们的单例模式 。
所以最后的代码如下(已经将业务代码与管理代码分离):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 let getSingle = function (fn ){ let result return function ( ){ return result || (result = fn.apply (this , arguments )) } } let createLoginLayer = function ( ){ let div = document .createElement ('div' ) div.innerHTML = '登录框' document .body .appendChild (div) return div } let createSingleLoginLayer = getSingle (createLoginLayer)document .getElementById ('loginBtn' ).onclick = function ( ){ let loginLayer = createSingleLoginLayer () loginLayer.style .display = 'block' } document .getElementById ('cancelBtn' ).onclick = function ( ){ let loginLayer = createSingleLoginLayer () loginLayer.style .display = 'none1 }
总结 单例模式是一个比较重要的单例模式,在进行底层开发或者注重性能的任务中,单例模式可以节省很多消耗,提高性能。
参考