- 什么是单例模式?
- 单例模式的实现。
- JavaScript中的单例模式。
什么是单例模式?
保证一个类仅有一个实例,并提供一个访问它的全局访问点。
单例模式的实现
根据单例模式的特点,一个类仅有一个实例。实现单例模式无非通过一个标志变量判断该类是否创建过对象
- 如果创建过,就返回已创建的对象
- 否则,创建一个新对象并返回。
以上是单例模式的一个简单实现,通过// 一个简单的单例模式
let Singleton = function(name) {
this.name = name
this.instance = null
}
Singleton.getInstance = function(name) {
if(!this.instance) {
this.instance = new Singleton(name)
}
return this.instance
}
let singleton = Singleton.getInstance('bayue')
let singleton1 = Singleton.getInstance('bayue01')
console.log(singleton === singleton1); // true
Singleton
的getInstance()
方法返回该对象的实例。在方法内部判断该对象上的instance
属性(标志变量)是否为null
,如果是null
,则创建一个新的对象并返回。
两种写法都可以实现单例模式,个人认为,第一种写法在// 单例模式的另一种写法
// ...
Singleton.getInstance = (function () {
let instance = null
return function(name) {
if(!instance){
instance = new Singleton(name)
}
return instance
}
})()
// ...
Singleton实例
中,始终有一个instance
属性,而第二种写法,只有在需要时才会声明instance
变量(惰性单例)。但第一种写法看起来更直观一些。
透明的单例模式
通过上述实现,使用者必须知道该类是一个单例类,而且获取它的实例也需要用getInstance()
,增加了这个类的不透明度。
因此需要让该类在使用时和普通的类一样去使用。
let CreateDiv = (function() {
let instance = null
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
})()
这是一个透明的单例模式,但是该类的内部为了将instance
封装,明显增加了程序内部的复杂度,而且函数内部的也不符合单一职责原理,必然会造成高耦合。
如果未来需求要求将该类也能变成可以有多个实例的类时,就不得不去修改这个类本身(有可能会造成未知的bug),或者单独写一个应用于普通场景的类(代码冗余)。
利用代理实现单例模式
上述透明的单例模式存在的问题主要是:
- 函数内部不同功能耦合度高
- 类的可复用性差
通过代码可以看到CreateDiv
函数其实只负责两件事,一是检查该类的实例是否存在,二是初始化init()
。
让CreateDiv
只负责初始化逻辑,成为一个普通的类。
// CreateDiv在这里仅作为一个普通的类。
let 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)
}
如果要实现单例模式的话,通过代理的方式,来间接将作为普通类的CreateDiv
变为单例的类。
let ProxySingleton = (function() {
let instance = null
return function(html) {
if(!instance) {
instance = new CreateDiv(html)
}
return instance
}
})()
如果需要多个实例时,使用CreateDiv
就可以实现,如果需要将CreateDiv
变为单例类,使用ProxySingleton
类即可。
以上单例模式的代码更接近于面向对象语言中的实现。但JavaScript中并没有类的概念(在ES6中新增的class
关键字,其实只是一个语法糖),所以下面实现JavaScript中的单例模式。
JavaScript中的单例模式
根据单例模式的核心概念:确保只有一个实例,并提供全局访问点。
在JavaScript中,一个全局变量就满足单例模式的特点。
// 只有一个变量a
// 作为全局变量提供了全局的访问点
let a = {}
上述代码使用了ES6新增的关键字let
,如果用ES5中var
来声明变量,则很容易不经意间将变量覆盖。
为了确保全局变量的唯一和降低命名污染,有以下几种方式:
- 使用命名空间 ```javascript let namespace = { a: function() {}, b: function() {} } // 动态创建命名空间 let MyApp = {}
MyApp.namespace = function(name) { let parts = name.split(‘.’) // [a, b] let current = MyApp for(let i in parts) { if(!current[parts[i]]) { current[parts[i]] = {} } current = current[parts[i]] } } MyApp.namespace(‘event’) MyApp.namespace(‘dom.style’)
2. 使用**闭包封装**私有变量
```javascript
let User = (function () {
let __name, __age
return {
getUserInfo: function() {
return __name + '-' + __age
}
}
})()
惰性单例
惰性单例是指,在需要时才会创建对象实例。在单例模式的第二种实现中,就使用过惰性单例。
在实际应用中,常见的就是模态框的创建。通常来说,一个页面只会存在于一个模态框(不考虑多级操作的情况),所以这种场景就可以通过单例模式来实现。
let CreateModal = (function () {
let modal = null
return function (content) {
if(!model) {
modal = document.createElement('div')
modal.innerHTML = content || ''
modal.style.display = 'none'
document.body.appendChild('div')
}
return model
}
})()
通用的惰性单例
上述代码实现了一个模态框场景的惰性单例模式。但这段代码依然存在不少问题。
- 违反单一职责原则,在
CreateModal
中将创建元素和单例模式的判断都放在了一个函数中。 - 可扩展性不够,如果未来需要创建其他元素,就不得不cv这段代码,造成代码冗余。
实际上,模式就是将不变的部分和可变的部分分离的过程。
观察上述代码逻辑可知,主要分为两个部分:
- 管理单例的逻辑
- 创建对象的逻辑
管理单例的函数
/**
* @param fn 创建对象的函数,createDiv、createImg等
*/
let getSingle = function(fn) {
let result = null
return function () {
return result || result = fn.apply(this, arguments)
}
}
创建div对象的方法
let createDiv = function (content) {
let div = document.createElement('div')
div.innerHTML = content
div.style.display = 'none'
document.body.appendChild('div')
console.log('run') // 因为是单例模式,所以只执行一次
return div
}
最终代码如下
let createSingle = getSingle(createDiv)
// click函数
button.onclick = function () {
createSingle()
}