javascript设计模式与开发实践阅读笔记——单例模式
Posted 出世Sunny
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了javascript设计模式与开发实践阅读笔记——单例模式相关的知识,希望对你有一定的参考价值。
定义
单例模式:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
具体来说,就是保证有些对象有且只有一个,比如线程池、全局缓存、浏览器中的window 对象等。在js中单例模式用途很广,比如登录悬浮窗,我希望无论我点击多少次这个浮窗都只会被创建一次,这里就可以用单例模式。
1.实现单例模式
思路:用一个变量来标志当前是否已经为某个类创建过对象,如果是,则在下一次获取该类的实例时,直接返回之前创建的对象;如果否就创建出那个对象。
1 var Singleton = function( name ){ //构造函数
2 this.name = name;
3 this.instance = null;
4 };
5 Singleton.prototype.getName = function(){ //构造器原形上添加方法,可以获得对象的name属性
6 alert ( this.name );
7 };
8 Singleton.getInstance = function( name ){
9 if ( !this.instance ){ //如果不存在对象实例
10 this.instance = new Singleton( name ); //创建对象实例
11 }
12 return this.instance; //返回对象实例
13 };
14 var a = Singleton.getInstance( ‘sven1‘ );
15 var b = Singleton.getInstance( ‘sven2‘ );
16 alert ( a === b ); // true
17 console.log(a.name); // sven1
18 console.log(b.name); // seven1
19 console.log(a.instance); // null
20 console.log(b.instance); // null
21 console.log(window.instance); // Singleton {name: "sven1", instance: null}
这是书里的例子,从最下面的测试来看,这个例子其实并不太好,比较容易让人误会。我们改造一下,其实是一个意思:
1 var Singleton = function( name ){ //构造函数
2 this.name = name;
3 this.instance = null; //无效的一个属性
4 };
5 Singleton.prototype.getName = function(){ //构造器原型上添加方法,可以获得对象的name属性
6 alert ( this.name );
7 };
8 create = function( name ){ //全局创建对象的函数
9 if ( !this.sing ){ //这里的this指向的是window,即全局,如果全局不存在sing对象
10 this.sing = new Singleton( name ); //创建sing对象
11 }
12 return this.sing; //返回sing对象
13 };
14 var a = create( ‘sven1‘ );
15 var b = create( ‘sven2‘ );
16 alert ( a === b ); // true
17 console.log(a.name); // sven1
18 console.log(b.name); // seven1
19 console.log(a.instance); // null
20 console.log(b.instance); // null
21 console.log(window.sing); // Singleton {name: "sven1", instance: null}
书里还有第二个创建单例模式的例子,如下:
1 var Singleton = function( name ){ //构造函数
2 this.name = name;
3 };
4 Singleton.prototype.getName = function(){ //原型上添加一个方法,可以返回对象的name属性
5 alert ( this.name );
6 };
7 Singleton.getInstance = (function(){ //全局的一个自执行函数,自执行是为了把返回的函数字面量赋给Singleton.getInstance
8 var instance = null; //函数内部变量,但用闭包保存起来
9 return function( name ){
10 if ( !instance ){ //如果没有创建过对应对象,即函数的这个内部变量没有被赋值
11 instance = new Singleton( name ); //创建对象
12 }
13 return instance; //返回对象
14 }
15 })();
这个例子比上面的要好不少,作者表示之所以用Singleton.getInstance这样的命名,是故意的,故意用这样的方式来创建单例类,来和通过new XXX的方式获取到的对象区分开,创建单例类必须要保存单例,所以需要定义一个全局变量,这种方式其实很不友好。
2.改进单例模式
现在的目标是实现一个“透明”的单例类,利用闭包保存单例,用户从这个类中创建对象的时候,可以像使用其他任何普通类一样,这种方式较上面友好许多。
1 var CreateDiv = (function(){ //匿名自执行函数,同时返回一个函数,创造了一个闭包环境
2 var instance; //利用闭包存储的对象实例
3 var CreateDiv = function( html ){ //返回的函数
4 if ( instance ){ //如果对象存在,返回对象
5 return instance;
6 }
7 this.html = html; //不存在就赋值,创建
8 this.init();
9 return instance = this; //new的时候返回实例
10 };
11 CreateDiv.prototype.init = function(){ //原型上绑定的方法
12 var div = document.createElement( ‘div‘ );
13 div.innerHTML = this.html;
14 document.body.appendChild( div );
15 };
16 return CreateDiv;
17 })();
18
19 var a = new CreateDiv( ‘sven1‘ );
20 var b = new CreateDiv( ‘sven2‘ );
21 alert ( a === b ); // true
缺点:为了把instance封装起来,我们使用了自执行的匿名函数和闭包,并且让这个匿名函数返回真正的Singleton 构造方法,这增加了一些程序的复杂度,阅读起来也不是很舒服。
1 var CreateDiv = function( html ){
2 if ( instance ){
3 return instance;
4 }
5 this.html = html;
6 this.init();
7 return instance = this;
8 };
观察这段构造函数,它实际上做了两件事,第一是创建对象和执行初始化init方法,第二是保证只有一个对象。这其实是种不好的做法,应该尽量遵循“单一职责原则”,假设我们某天需要利用这个类,在页面中创建千千万万的div,即要让这个类从单例类变成一个普通的可产生多个实例的类,那我们必须得改写CreateDiv 构造函数,把控制创建唯一对象的那一段去掉,这种修改会给我们带来不必要的烦恼。
3.用代理实现单例模式
通过引入代理类的方式,来解决上面提到的问题。
在CreateDiv 构造函数中,把负责管理单例的代码移除出去,使它成为一个普通的创建div的类,如下:
1 var CreateDiv = function( html ){
2 this.html = html;
3 this.init();
4 };
5 CreateDiv.prototype.init = function(){
6 var div = document.createElement( ‘div‘ );
7 div.innerHTML = this.html;
8 document.body.appendChild( div );
9 };
接下来引入代理类SingletonCreateDiv:
1 var SingletonCreateDiv = (function(){ 2 var instance; 3 return function( html ){ 4 if ( !instance ){ 5 instance = new CreateDiv( html ); 6 } 7 return instance; 8 } 9 })(); 10 var a = new SingletonCreateDiv( ‘sven1‘ ); 11 var b = new SingletonCreateDiv( ‘sven2‘ ); 12 alert ( a === b ); //true
通过引入代理类的方式,完成了一个单例模式的编写,跟之前不同的是,现在我们把负责管理单例的逻辑移到了代理类SingletonCreateDiv中。这样一来,CreateDiv就变成了一个普通的类,它跟SingletonCreateDiv组合起来可以达到单例模式的效果。本例是缓存代理的应用之一,这样写的好处是毋庸置疑的。
4.javascript中的单例模式
前面几种单例模式的实现,更多的是接近传统面向对象语言中的实现,单例对象从“类”中创建而来。在以类为中心的语言中,这是很自然的做法,对象总是从类中创建而来的。而在JavaScript 中创建对象的方法非常简单,既然我们只需要一个“唯一”的对象,为什么要为它先创建一个“类”呢?这无异于穿棉衣洗澡,传统的单例模式实现在JavaScript中并不适用。
单例模式的核心是确保只有一个实例,并提供全局访问。
全局变量不是单例模式,但在JavaScript 开发中,我们经常会把全局变量当成单例来使用。
var a = {};
当用这种方式创建对象a 时,对象a 确实是独一无二的。如果a变量被声明在全局作用域下,则我们可以在代码中的任何位置使用这个变量,全局变量提供给全局访问是理所当然的。这样就满足了单例模式的两个条件。
但这样明显会污染全局的命名空间,有这样几种方式可以相对降低其它全局变量带来的命名污染:
(1)使用命名空间
变量放到了命名空间内,成为了命名空间的属性
1 var namespace1 = {
2 a: function(){
3 alert (1);
4 },
5 b: function(){
6 alert (2);
7 }
8 };
(2)使用闭包封装私有变量
这种方法把一些变量封装在闭包的内部,只暴露一些接口跟外界通信。
1 var user = (function(){
2 var __name = ‘sven‘,
3 __age = 29;
4 return {
5 getUserInfo: function(){
6 return __name + ‘-‘ + __age;
7 }
8 }
9 })();
5.惰性单例
惰性单例指的是在需要的时候才创建对象实例。惰性单例是单例模式的重点,这种技术在实际开发中非常有用,有用的程度可能超出了我们的想象。
首先我们想要创建一个悬浮窗用于登录,被一个点击事件触发,且悬浮窗唯一:
1 var loginLayer = (function(){ //loginLayer就是单例对象,这里没有用类的方式创建,而是直接给了一个全局对象
2 var div = document.createElement( ‘div‘ );
3 div.innerHTML = ‘我是登录浮窗‘;
4 div.style.display = ‘none‘;
5 document.body.appendChild( div );
6 return div;
7 })();
8
9 document.getElementById( ‘loginBtn‘ ).onclick = function(){
10 loginLayer.style.display = ‘block‘;
11 };
这个方法缺点很明显,如果我们一直不去登录,由于这个悬浮窗是早就创建好的,这样就有可能浪费一个DOM节点。应该用户点击之后才创建:
1 var createLoginLayer = function(){
2 var div = document.createElement( ‘div‘ );
3 div.innerHTML = ‘我是登录浮窗‘;
4 div.style.display = ‘none‘;
5 document.body.appendChild( div );
6 return div;
7 };
8 document.getElementById( ‘loginBtn‘ ).onclick = function(){
9 var loginLayer = createLoginLayer();
10 loginLayer.style.display = ‘block‘;
11 };
这次达到了惰性的目的,但是失去了单例效果,每次点击都会创建一个悬浮窗。我们可以用一个变量来判断是否已经创建过登录浮窗:
1 var createLoginLayer = (function(){ //自执行创建闭包,保存实例
2 var div; //实例
3 return function(){
4 if ( !div ){ //如果实例不存在,创建实例
5 div = document.createElement( ‘div‘ );
6 div.innerHTML = ‘我是登录浮窗‘;
7 div.style.display = ‘none‘;
8 document.body.appendChild( div );
9 }
10 return div; //返回实例
11 }
12 })();
13
14 document.getElementById( ‘loginBtn‘ ).onclick = function(){
15 var loginLayer = createLoginLayer();
16 loginLayer.style.display = ‘block‘;
17 };
这次代码虽然实现了功能,但是又违反了之前提过的“单一职责原则”,我们需要把管理单例的代码抽离出来:
1 var getSingle = function( fn ){ //管理单例,fn为创建一个对象的函数
2 var result;
3 return function(){
4 return result || ( result = fn .apply(this, arguments ) );
5 }
6 };
7
8 var createLoginLayer = function(){ //创建悬浮窗
9 var div = document.createElement( ‘div‘ );
10 div.innerHTML = ‘我是登录浮窗‘;
11 div.style.display = ‘none‘;
12 document.body.appendChild( div );
13 return div;
14 };
15
16 var createSingleLoginLayer = getSingle( createLoginLayer ); //创建一个单例悬浮窗函数
17
18 document.getElementById( ‘loginBtn‘ ).onclick = function(){
19 var loginLayer = createSingleLoginLayer(); //调用创建单例的函数
20 loginLayer.style.display = ‘block‘;
21 };
利用单例模式还可以完成事件代理,只绑定一次事件
1 var bindEvent = getSingle(function(){
2 document.getElementById( ‘div1‘ ).onclick = function(e){
3 alert ( ‘e.target‘ );
4 }
5 return true; //单例需要接收一个返回值
6 });
7 var render = function(){
8 console.log( ‘开始渲染列表‘ );
9 bindEvent();
10 };
11
12 render(); //这里即使运行了三次,但只绑定了一次,不会浪费性能
13 render();
14 render();
总结
单例模式是一种简单但非常实用的模式,特别是惰性单例技术,在合适的时候才创建对象,并且只创建唯一的一个。将管理单例和创建对象的方法分开是种很好的思路。
以上是关于javascript设计模式与开发实践阅读笔记——单例模式的主要内容,如果未能解决你的问题,请参考以下文章
javascript设计模式与开发实践阅读笔记——迭代器模式