小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。
前言
记录自己学习设计模式,内容来自
《JavaScript设计模式与开发实践》
单例模式的定义
保证一个类仅有一个实例,并提供一个访问它的全局访问点
单例模式是一种常见的模式,有一个对象我们往往只需要一个,比如线程池、全局缓存、浏览器中的window对象等。在JavaScript开发中,单例模式用途同样广泛。试想一下,当我们单机登录按钮的时候,页面中会出现一个弹窗,而这个弹窗是唯一的,无论单机多少次登录按钮,这个浮窗都只会创建一次,那么这个登录浮窗就适合用单例模式来创建
普通的单例模式
const Singleton = function(name) {
this.name = name
}
Singleton.instance = null
Singleton.prototype.getName = function() {
alert(this.name)
}
Singleton.getInstance = function(name) {
if (!this.instance) {
this.instance = new Singleton(name)
}
return this.instance
}
const a = Singleton.getInstance('a')
const b = Singleton.getInstance('b')
console.log(a === b) // true
上面虽然已经完成了一个单例模式的编写,但是这段单例模式代码的意义不大
透明的单例模式
const CreateDiv = (function() {
let instance;
const CreateDiv = function(html) {
if (instance) {
return instance
}
this.html = html
this.init()
return instance = this
}
CreateDiv.prototype.init = function() {
const div = document.createElement('div')
div.innerHTML = this.html
document.body.appendChild(div)
}
return CreateDiv
})()
const a = new CreateDiv('a')
const b = new CreateDiv('b')
console.log(a, b)
console.log(a === b)
虽然现在完成了一个透明的单例类的编写,但他同样有一些缺点,使用了自执行的匿名函数和闭包,增加了程序的复杂度,阅读起来不舒服
const CreateDiv = function(html) {
if (instance) {
return instance
}
this.html = html
this.init()
return instance = this
}
在这段代码中,createDiv做了两件事情,第一是创建对象和执行初始化init方法,第二是保证只有一个对象,不符合单一职责原则
假设我们某天需要利用这个类创建千万个div,那么我们必须改写CreateDiv构造函数,这种修改会给我们带来不必要的烦恼
用代理实现单例模式
const CreateDiv = function(html) {
this.html = html
this.init()
}
CreateDiv.prototype.init = function() {
const div = document.createElement('div')
div.innerHTML = this.html
document.body.appendChild(div)
}
接下来写代理类
const ProxySingletonCreateDiv = (function() {
let instance;
return function(html) {
if (!instance) {
instance = new CreateDiv('a')
}
return instance
}
})()
const a = new ProxySingletonCreateDiv('a')
const b = new ProxySingletonCreateDiv('b')
console.log(a, b)
console.log(a === b)
通过代理类的方式,我们同样完成了单例模式的编写,跟之前不同的是,现在我们把负责管理单例的逻辑移到了代理类中,这样一来,createDiv就变成了普通的类,跟代理组合起来达到单例的效果
JavaScript中的单例
前面的几种单例模式的实现,更多的是接近传统面向对象语言中的实现,单例模式从"类"中创建而来
使用命名空间
const MyApp = {}
MyApp.namespace = function(name) {
const parts = name.split('.')
let current = MyApp
for (let key of parts) {
if (!current[key]) {
current[key] = {}
}
current = current[key]
}
}
myApp.namespace('event')
myApp.namespace('dom.style')
// 上述代码等同于
var MyApp = {
event: {},
dom: {
style: {}
}
}
使用闭包封装私有变量
var user = (function() {
const _name = 'jie',
_age = '22';
return {
getUserInfo() {
return _name + '-' + _age
}
}
})()
惰性单例
惰性单例是指在需要的时候才创建对象实例,这种技术在实际开发中非常有用,有用的程度超乎我们想象
例如创建一个唯一的弹窗
const getLoginLayer = (function() {
let div;
return function() {
if (!div) {
div = document.createElement('div')
div.style.display = 'none'
div.innerHTML = '我是弹窗'
div.classList.add('popup')
document.body.appendChild(div)
}
return div
}
})()
const open = document.getElementById('open')
const close = document.getElementById('close')
open.onclick = function() {
console.log('我点击了')
const loginLayer = getLoginLayer()
loginLayer.style.display = 'block'
}
close.onclick = function() {
const loginLayer = getLoginLayer()
loginLayer.style.display = 'none'
}
这个div只会被创建一次
通用的惰性单例
- 上面的这段代码是违反单一职责原则,创建对象和管理单例的逻辑都在getLoginLayer对象每部
- 如果我们下次需要创建页面中唯一的iframe,或者script标签,就得如法炮制,把函数照抄一遍
const getIframe = (function() {
let iframe;
return function() {
if (!iframe) {
iframe = document.createElement('iframe')
iframe.style.display = 'none'
document.body.appendChild(iframe)
}
return iframe
}
})()
现在我们把这个创建的逻辑抽离出来
const getSingle = function(fn) {
let result;
return function() {
return result || (result = fn.apply(this, arguments))
}
}
我们将创建函数参数fn的方式传入getSingle,这样不仅可以创建loginLayer,还能创建script,iframe,之后再让getSingle返回一个新的函数,用变量来保存fn的返回结果,result在闭包中,永远不会被销毁。
const getSingle = function(fn) {
let result;
return function() {
return result || (result = fn.apply(this, arguments))
}
}
const getLoginLayer = function() {
const div = document.createElement('div')
div.style.display = 'none'
div.innerHTML = '我是弹窗'
div.classList.add('popup')
document.body.appendChild(div)
return div
}
const createSingleLoginLayer = getSingle(getLoginLayer)
open.onclick = function() {
console.log('我点击了')
const loginLayer = createSingleLoginLayer()
loginLayer.style.display = 'block'
}
close.onclick = function() {
const loginLayer = createSingleLoginLayer()
loginLayer.style.display = 'none'
}
下面让我们创建唯一的iframe动态加载第三方页面
open.onclick = function() {
const iframe = createSingleIframe()
iframe.style.display = 'block'
iframe.src = 'https://www.bilibili.com/'
}
close.onclick = function() {
const iframe = createSingleIframe()
iframe.style.display = 'none'
}
单例模式的用途不止可以创建对象,比如我们通常渲染完页面中的一个列表之后,接下来要给这个列表绑定click事件,如果是通过ajax动态添加列表,在使用事件代理的前提下,click事件实际上只需要在第一次渲染的时候被绑定一次,但是我们不像去判断当前是否是第一次渲染列表,如果借助Jquery,我们通常给节点绑定one事件。
var bindEvent = function() {
$('div').one('click', function() {
alert('click)
})
}
var render = function() {
bindEvent()
}
render()
render()
render()
如果利用getSingle函数,也能达到一样的效果。代码如下
var bindEvent = getSingle(function() {
console.log('我执行了内部')
document.getElementById('div1').addEventListener('click', function() {
alert('click')
})
return true
})
var render = function() {
console.log('我执行了')
bindEvent()
}
render()
render()
render()
可以看到,render函数和bindEvent分别执行了3次,但div实际上只绑定了一次事件