JavaScript设计模式-创建型-单例模式
1. 什么是设计模式
定义
单例模式的定义是:保证一个类仅有一个实例,并提供一个访问它的全局访问点。也可以用于一个对象来规划一个命名空间,管理对象上的属性与方法。
1.1 实现单例模式
实现一个单例模式很简单,只需要用一个变量标识来记录实例是否存在,如果存在将当前实例返回。
ES5
/**
* [Singleton 单例模式构造函数]
*/
var Singleton = function (name) {
// 对象公有属性
this.name = name
}
/**
* [getName 类原型公有方法]
*/
Singleton.prototype.getName = function () {
return this.name
}
/**
* [getInstance 类静态公有方法]
*/
Singleton.getInstance = function (name) {
/**
* [如果该静态属性没有被实例]
* @param {[type]} !this.instance [为类静态公有属性]
* @return {[type]} [永远返回赋值静态公有属性instance === Singleton实例]
*/
if (!this.instance) {
this.instance = new Singleton(name)
}
return this.instance
}
var a = Singleton.getInstance('single1')
var b = Singleton.getInstance('single2')
console.log(a === b)// true
ES6
/**
* Singleton 单例模式 类
*/
class Singleton {
/**
* [constructor 类的构造函数]
*/
constructor (name) {
this.name = name
}
/**
* [getInstance 静态方法]
*/
static getInstance (name) {
if (!this.instance) {
this.instance = new Singleton(name)
}
return this.instance
}
/**
* [getName 类原型公有方法]
*/
getName () {
return this.name
}
}
var a = Singleton.getInstance('single1')
var b = Singleton.getInstance('single2')
console.log(a === b)// true
通过Singleton.getInstance
来获取Singleton
单例类的唯一实例对象。如果Singleton
构造函数静态属性instance
不存在,将赋值Singleton
实例对象返回,存在将直接返回Singleton
实例对象。通过这种方式来创建单例模式,会给这个类增加“不透明性”,在使用这个类时,开发人员需要知道这个类是跟以往new XXX
实例化对象不同,而是要通过Singleton.getInstance
来获取唯一的实例对象。
1.2 透明的单例模式
实现一个“透明”的单例类,在实例化对象的时候可以向普通类一样使用new XXX
。
来实现一个在页面上创建一个唯一div
节点的“透明”单例类
/**
* [CreateDiv 透明单例类,函数表达式]
* @return {[type]} [立即执行函数 返回CreateDiv单例类]
*/
var CreateDiv = (function () {
var instance
var CreateDiv = function (html) {
if (instance) {
return instance
}
this.html = html
this.init()
return instance = this
}
CreateDiv.prototype.init = function () {
var div = document.createElement('div')
div.innerHTML = this.html
document.body.appendChild(div)
}
return CreateDiv
})()
var a = new CreateDiv('hello')
var b = new CreateDiv('hi')
console.log(a === b)
使用匿名立即执行函数,通过闭包的方式,将instance
作为是否被实例化状态存储在闭包环境中,返回了真正的Singleton
单例类构造方法。实现了在实例对象时可以使用new XXX
,但是同样存在缺点,代码阅读起来不是很优雅。
CreateDiv
构造函数在被new
实例化时,主要实现了两个功能
- 创建实例对象,赋值给闭包局部变量
instance
并返回,执行了init
方法。 - 保证只返回一个实例对象,通过读取闭包中
instance
实例,只返回唯一的实例对象
1.3 用代理实现单例模式
基于“透明的单例模式”,如果我想要用这个类,创造很多的单例类,也就是我想创建很多不一样的div
,那就需要将CreateDiv
类进行改造,把它变成只做创建的工作,然后使用代理来控制只创建唯一的单例。
var CreateDiv = function (html) {
this.html = html
this.init()
}
CreateDiv.prototype.init = function () {
var div = document.createElement('div')
div.innerHTML = this.html
document.body.appendChild(div)
}
var ProxySingletonCreateDiv = (function () {
var instance
return function (html) {
if (!instance) {
instance = new CreateDiv(html)
}
return instance
}
})()
var ProxySingletonCreateDivAnother = (function () {
var instance
return function (html) {
if (!instance) {
instance = new CreateDiv(html)
}
return instance
}
})()
var a = new ProxySingletonCreateDiv('hello')
var b = new ProxySingletonCreateDiv('hi')
var c = new ProxySingletonCreateDivAnother('first')
var d = new ProxySingletonCreateDivAnother('second')
console.log(a === b)// true
console.log(c === d)// true
把控制单例的逻辑放在代理类上,CreateDiv
类作为普通类,只做创建div
动作。这样一个CreateDiv
类和代理类组合,也可以实现单例类。代码也更加的解耦。
1.4 惰性单例
惰性单例指的是在需要的时候才会去实例对象
比如说,要实现一个登陆dialog
功能,点击页面上“去登录”按钮,会弹出唯一的登录框。
简单的方案是页面加载完后这个创建这个登录框,登录框此时是隐藏状态。当点击去登录按钮时,显示登录框。
<html lang="en">
<body>
<button id="loginBtn">去登录</button>
</body>
<script>
var loginModalBox = (function () {
var modalBox = document.createElement('div')
modalBox.innerHTML = '登录框'
modalBox.style.display = 'none'
document.body.appendChild(modalBox)
return modalBox
})()
document.getElementById('loginBtn').onclick = function() {
loginModalBox.style.display = 'block'
}
</script>
</html>
这种写法的确缺点是,页面加载完毕就创建了很多节点,而不是在需要创建的时候去创建。
使用惰性单例对上面代码进行改造
<html lang="en">
<body>
<button id="loginBtn">去登录</button>
</body>
<script>
var loginModalBox = (function () {
var modalBox
return function () {
if (!modalBox) {
modalBox = document.createElement('div')
modalBox.innerHTML = '登录框'
modalBox.style.display = 'none'
document.body.appendChild(modalBox)
}
return modalBox
}
})()
document.getElementById('loginBtn').onclick = function() {
var modal = loginModalBox()
modal.style.display = 'block'
}
</script>
</html>
1.5 通用的惰性单例
基于惰性单例案例,可以看出惰性单例案例代码存在的问题。
- 显然是违反了单一职责原则。创建对象和管理单例的逻辑都放在了
loginModalBox
对象内部。 - 如果我们需要在页面创建其他唯一的标签元素,比如“iframe”或者“script”,就需要把
loginModalBox
函数照抄很多次。
<html lang="en">
<body>
<button id="loginBtn">去登录</button>
<button id="iframeBtn">显示iframe元素</button>
</body>
<script>
var loginModalBox = (function () {
var modalBox
return function () {
if (!modalBox) {
modalBox = document.createElement('div')
modalBox.innerHTML = '登录框'
modalBox.style.display = 'none'
document.body.appendChild(modalBox)
}
return modalBox
}
})()
var iframeModalBox = (function () {
var modalBox
return function () {
if (!modalBox) {
modalBox = document.createElement('iframe')
document.body.appendChild(modalBox)
}
return modalBox
}
})()
document.getElementById('loginBtn').onclick = function() {
var modal = loginModalBox()
modal.style.display = 'block'
}
document.getElementById('iframeBtn').onclick = function() {
var modal = iframeModalBox()
modal.src = 'https://baidu.com'
}
</script>
</html>
我们需要把不变的部分抽离出来,不考虑一个登录框单例,和iframe
层单例直接有多少差异。他们有共同不变的地方,那就是只能被创建一次。也就是说我们完全可以把管理单例逻辑抽离封装。创建元素对象的功能是不同的,是可变的,我们可以对这块进行单独的处理。
管理单例的逻辑很简单,使用一个变量来标识是否创建过对象,如果是则返回之前创建好的。
var obj
if (!obj) {
obj = xxx
}
return obj
将管理单例逻辑封装在getSingle
函数中
var getSingle = function (fn) {
var result
return function () {
return result || ( result = fn.apply(this, arguments) )
}
}
函数接收fn
创建元素对象方法作为参数。函数内部定义变量result
来获取创建元素对象,该对象也是判断元素是否被创建过的唯一标识。返回匿名函数交给调用者来决定何时使用,此时result
变量是在闭包中,不会被销毁。在外部使用匿名函数来创建元素对象时,如果result
为空,就会执行创建元素对象函数,并赋值给闭包变量result
。之后多次调用匿名函数直接返回存在的元素对象,完成了单例的功能。
现在可以创建任何想要的单例元素了
<html lang="en">
<body>
<button id="loginBtn">去登录</button>
<button id="iframeBtn">显示iframe元素</button>
</body>
<script>
var getSingle = function (fn) {
var result
return function () {
return result || ( result = fn.apply(this, arguments) )
}
}
var createLoginModalBox = function () {
var modalBox = document.createElement('div')
modalBox.innerHTML = '登录框'
modalBox.style.display = 'none'
document.body.appendChild(modalBox)
return modalBox
}
var createIframeModalBox = function () {
var modalBox = document.createElement('iframe')
document.body.appendChild(modalBox)
return modalBox
}
var createSingleLoginModalBox = getSingle(createLoginModalBox)
var createSingleIframeModalBox = getSingle(createIframeModalBox)
document.getElementById('loginBtn').onclick = function() {
var modal = createSingleLoginModalBox()
modal.style.display = 'block'
}
document.getElementById('iframeBtn').onclick = function() {
var modal = createSingleIframeModalBox()
modal.src = 'https://baidu.com'
}
</script>
</html>
2. 使用场景
2.1 命名空间管理
一个项目肯能是多人共同开发,这个时候会遇到命名冲突的问题
// A 开发者
function findEl () {}
function html () {}
function append () {}
function addClass () {}
// B开发者
var html = 'welcome!'
随着项目时间的推移,各种工具业务函数也会越来越多,多人开发的时候,很有可能命名被覆盖。
命名空间可以为我们减少冲突的发生,将一些属性和方法包装在一个对象中。
// A 开发者
var developA = {
findEl: function () {},
html: function () {},
append: function () {},
addClass: function () {}
}
// B开发者
var developB = {
html: 'welcome!'
}
2.2 模块管理
一个应用程序是很多模块组成,比如网络请求,DOM操作,事件管理等。单例模式可以用来管理这些模块
var utils = (function() {
// ajax模块
var ajax = {
get: function(url, params) {},
post: function(url, params) {}
}
// dom模块
var dom = {
get: function(target) {},
create: function(el, children) {}
}
// event模块
var event = {
add: function(type, fn) {},
remove: function(type, fn) {}
}
return {
ajax: ajax,
dom: dom,
event: event
}
})()
3. 收获与总结
单例模式的核心是确保只有一个实例, 并提供全局访问。JavaScript代码书写灵活,在没有严格的规范下,实用单例模式进行命名空间,管理模块是一个很好的开发习惯。能够减少命名的冲突。
单例模式是一种简单但非常实用的模式,惰性单例技术在适合的时候创建唯一实例对象,减少了运行开支。还有就是将管理单例职责和创建对象分成两个不同方法,组合实用让单例模式更具魅力。