JavaScript设计模式-创建型-单例模式

2021-05-06 JavaScript设计模式 阅读 742 次

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实例化时,主要实现了两个功能

  1. 创建实例对象,赋值给闭包局部变量instance并返回,执行了init方法。
  2. 保证只返回一个实例对象,通过读取闭包中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 惰性单例

惰性单例指的是在需要的时候才会去实例对象

JavaScript设计模式-创建型-单例模式-黄继鹏博客

比如说,要实现一个登陆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 通用的惰性单例

基于惰性单例案例,可以看出惰性单例案例代码存在的问题。

  1. 显然是违反了单一职责原则。创建对象和管理单例的逻辑都放在了loginModalBox对象内部。
  2. 如果我们需要在页面创建其他唯一的标签元素,比如“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代码书写灵活,在没有严格的规范下,实用单例模式进行命名空间,管理模块是一个很好的开发习惯。能够减少命名的冲突。

单例模式是一种简单但非常实用的模式,惰性单例技术在适合的时候创建唯一实例对象,减少了运行开支。还有就是将管理单例职责和创建对象分成两个不同方法,组合实用让单例模式更具魅力。

0条评论
...