• 探索Javascript设计模式---单例模式


    最近打算系统的学习javascript设计模式,以便自己在开发中遇到问题可以按照设计模式提供的思路进行封装,这样可以提高开发效率并且可以预先规避很多未知的问题。

    先从最基本的单例模式开始。

    什么是单例模式:

    单例模式,从名字拆分来看,单指的是一个,例是实例,意思是说多次通过某个类创造出来实例始终只返回同一个实例,它限制一个类只能有一个实例。单例模式主要是为了解决对象的创建问题。单例模式的特点:

    1. 一个类只有一个实例
    2. 对外提供唯一的访问接口

    在一些以类为核心的语言中,例如java,每创建一个对象就必须先定义一个类,对象是从类创建而来。js是一门无类(class-free)的语言,在js中创建对象的方法非常简单,不需要先定义类即可创建对象。

    在js中,单例模式是一种常见的模式,例如浏览器中提供的window对象,处理数字的Math对象。

    单例模式的实现

    1. 对象字面量

    在js中实现单例最简单的方式是创建对象字面量,字面量对象中可以包含多个属性和方法。

    var mySingleton = {
    	attr1:1,
    	attr2:2,
    	method:function (){
    		console.log("method");	
    	}
    }
    

    以上创建一个对象,放在全局中,就可以在任何地方访问,要访问对象中的属性和方法,必须通过mySingleton这个对象,也就是说提供了唯一一个访问接口。

    2. 使用闭包私有化

    扩展mySingleton对象,添加私有的属性和方法,使用闭包的形式在其内部封装变量和函数声明,只暴露公共成员和方法。

    var mySingleton = (function (){
    	//私有变量
    	var privateVal = '我是私有变量';
    	//私有函数
    	function privateFunc(){
    		console.log('我是私有函数');	
    	}
    
    	return {
    			attr1:1,
    			attr2:2,
    			method:function (){
    				console.log("method");	
    				privateFunc();
    			}
    		}	
    })()
    

    privateValprivateVal被封装在闭包产生的作用域中,外界访问不到这两个变量,这避免了对全局命名污染。

    3.惰性单例

    无论使用对象字面量或者闭包私有化的方式创建单例,都是在脚本一加载就被创建。有时候页面可能不会用到这个单例对象,这样就会造成资源浪费。对于这种情况,最佳处理方式是使用惰性单例,也就是在需要这个单例对象时再初始化。

    var mySingleton = (function (){
    	function init(){
    		//私有变量
    		var privateVal = '我是私有变量';
    		//私有函数
    		function privateFunc(){
    			console.log('我是私有函数');	
    		}
    
    		return {
    			attr1:1,
    			attr2:2,
    			method(){
    				console.log("method");	
    				privateFunc();
    			}
    		}
    	}
    
    	//用来保存创建的单例对象
    	 var instance = null;
    	return {
    		getInstance (){
    			//instance没有存值,就执行函数得到对象
    			if(!instance){
    				instance = init();
    			}	
    			//instance存了值,就返回这个对象
    			return instance;
    		}
    	}
    })();
    
    //得到单例对象
    var singletonObj1 = mySingleton.getInstance();
    var singletonObj2 = mySingleton.getInstance();
    
    console.log( singletonObj1 === singletonObj2 ); //true
    

    程序执行后,将创建单例对象的代码封装到init函数中,只暴露了获取单例对象的函数getInstance。当有需要用到时,通过调用函数mySingleton.getInstance()得到单例对象,同时使用instance将对象缓存起来,再次调用mySingleton.getInstance()后得到的是同一个对象,这样通过一个函数不会创建多个对象,起到节省资源的目的。

    4. 使用构造函数

    可以使用构造函数的方式,创造单例对象:

    function mySingleton(){
    	//如果缓存了实例,则直接返回
    	if (mySingleton.instance) {
    		return mySingleton.instance;
    	}
    
    	//当第一次实例化时,先缓存实例
    	mySingleton.instance = this;
    
    }
    
    mySingleton.prototype.otherFunc = function (){
    	console.log("原型上其他方法");	
    }
    
    var p1 = new mySingleton();
    var p2 = new mySingleton();
    
    console.log( p1 === p2 );  //true
    

    当第一次使用new调用函数创建实例时,通过函数的静态属性mySingleton.instance把实例缓存起来,在第二次用new调用函数,判断实例已经缓存过了,直接返回,那么第一次得到的实例p1和第二次得到的实例p2是同一个对象。这样符合单例模式的特点:一个类只能有一个实例

    这样做有一个问题,暴露了可以访问缓存实例的属性mySingleton.instance,这个属性的值可以被改变:

    var p1 = new mySingleton();
    //改变mySingleton.instance的值
    //mySingleton.instance = null;
    //或者
    mySingleton.instance = {};
    var p2 = new mySingleton();
    console.log( p1 === p2 );  //false
    

    改变了mySingleton.instance值后,再通过new调用构造函数创建实例时,又会重新创建新的对象,那么p1p2就不是同一个对象,违反了单例模式一个类只能有一个实例。

    闭包中的实例

    不使用函数的静态属性缓存实例,而是重新改写构造函数:

    function mySingleton(){
    	//缓存当前实例
    	var instance  = this;
    
    	//执行完成后改写构造函数
    	mySingleton = function (){
    		return instance;	
    	}
    	//其他的代码
    	instance.userName = "abc";
    
    }
    
    mySingleton.prototype.otherFunc = function (){
    	console.log("原型上其他方法");	
    }
    
    var p1 = new mySingleton();
    var p2 = new mySingleton();
    
    console.log( p1 === p2 );  //true
    

    第一次使用new调用函数创建实例后,在函数中创建instance用来缓存实例,把mySingleton改写为另一个函数。如果再次使用new调用函数后,利用闭包的特性,返回了缓存的对象,所以p1p2是同一个对象。

    这样虽然也可以保证一个类只返回一个实例,但注意,第二次再次使用new调用的构造函数是匿名函数,因为mySingleton已经被改写:

    //第二次new mySingleton()时这个匿名函数才是真正的构造函数
    mySingleton = function (){
    	return instance;	
    }
    

    再次给原mySingleton.prototype上添加是属性,实际上这是给匿名函数的原型添加了属性:

    var p1 = new mySingleton();
    
    //再次给mySingleton的原型上添加属性
    mySingleton.prototype.addAttr = "我是新添加的属性";
    
    var p2 = new mySingleton();
    
    console.log(p2.addAttr); //undefined
    

    对象p2访问属性addAttr并没有找到。通过一个构造函数构造出来的实例并不能访问原型上的方法或属性,这是一种错误的做法,还需要继续改进。

    function mySingleton(){
    		
    	var instance;
    
    	//改写构造函数
    	mySingleton = function (){
    		return instance;	
    	}
    
    	//把改写后构造函数的原型指向this
    	
    	mySingleton.prototype = this;
    
    	//constructor改写为改写后的构造函数
    	mySingleton.prototype.constructor = mySingleton;
    
    	//得到改写后构造函数创建的实例
    	instance = new mySingleton;
    
    	//其他的代码
    	instance.userName = "abc";
    
    	//显示的返回改写后构造函数创建的实例
    	return instance;
    
    }
    
    mySingleton.prototype.otherFunc = function (){
    	console.log("原型上其他方法");	
    }
    
    var p1 = new mySingleton();
    
    //再次给mySingleton的原型上添加属性
    mySingleton.prototype.addAttr = "我是新添加的属性";
    
    var p2 = new mySingleton();
    
    console.log(p2.addAttr); //'我是新添加的属性'
    
    console.log( p1 === p2 );  //true
    

    以上代码主要做了以下几件事:

    1. 改写mySingleton函数为匿名函数
    2. 改写mySingleton的原型为第一次通过new创建的实例
    3. 因为改写了prototype,要把constructor指回mySingleton
    4. 显式返回通过改写后mySingleton构造函数构造出的实例

    无论使用多少次new调用mySingleton这个构造函数,都返回同一个对象,并且这些对象都共享同一个原型。

    实践单例模式

    1. 使用命名空间

    根据上述,在js中创建一个对象就是一个单例,把一类的方法和属性放在对象中,都通过提供的全局对象访问。

    var mySingleton = {
    	attr1:1,
    	attr2:2,
    	method:function (){
    		console.log("method");	
    	}
    }
    

    这样的方式耦合度极高,例如:要给这个对象添加属性:

    mySingleton.width = 1000;  //添加一个属性
    
    //添加一个方法会覆盖原有的方法
    mySingleton.method = function(){};
    

    如果在多人协作中,这样添加属性的方式经常出现被覆盖的危险,可以采用命名空间的方式解决。

    //A同学
    mySingleton.a = {};
    mySingleton.a.method = function(){}
    //访问
    mySingleton.a.method();
    
    //B同学
    mySingleton.b = {};
    mySingleton.b.method = function(){}
    //访问
    mySingleton.b.method();
    

    都在自己的命名空间中,覆盖的几率会很小。
    可以封装一个动态创建命名空间的通用方法,这样在需要独立的命名空间时只需要调用函数即可。

    mySingleton.namespace = function(name){
    	var arr = name.split(".");
    	//存一下对象
    	var currentObj = mySingleton;
    	for( var i = 0; i < arr.length; i++ ){
    		//如果对象中不存在,则赋值添加属性
    		if(!currentObj[arr[i]]){
    			currentObj[arr[i]] = {};
    		}
    		//把变量重新赋值,便于循环继续创建命名空间
    		currentObj = currentObj[arr[i]]
    	}
    }
    
    //创建命名空间
    mySingleton.namespace("bom");
    mySingleton.namespace("dom.style");
    

    以上调用函数生成命名空间的方式代码等价于:

    mySingleton.bom = {};
    mySingleton.dom = {};
    mySingleton.dom.style = {};
    

    2. 单例登录框

    使用面向对象实现一个登录框,在点击登录按钮后登录框被append到页面中,点击关闭就将登录框从页面中remove掉,这样频繁的操作DOM不合理也不是必要的。

    只需要在点击关闭时隐藏登录框,再次点击按钮后,只需要show出来即可。

    页面中只放一个按钮:

    <input type="button" value="登录" id="loginBtn" />
    

    js实现:

    function Login(){
    	var instance;
    
    	Login = function(){
    		return install;
    	}
    
    	Login.prototype = this;
    
    
    	install = new Login;
    
    	install.init();
    
    	return install;
    }
    
    Login.prototype.init = function(){
        //得到登录框元素
    	this.Login = this.createHtml();
    	document.body.appendChild(this.Login);
    	//绑定事件
    	this.addEvent();
    }
    Login.prototype.createHtml = function(){
    	var LoginDiv = document.createElement("div");
    	LoginDiv.className = "box";
    	var html = `<input type="button" value="关闭弹框" class="close" /><p>这里做登录</p>`
    
    	LoginDiv.innerHTML = html;
    
    	return LoginDiv;
    }
    Login.prototype.addEvent = function(){
    	var close = this.Login.querySelector(".close");
    	var _this = this;
    	close.addEventListener("click",function(){
    		_this.Login.style.display = 'none';
    	})
    }
    Login.prototype.show = function(){
    	this.Login.style.display = 'block';
    }
    //点击页面中的按钮
    var loginBtn = document.querySelector("#loginBtn");
    loginBtn.onclick = function(){
    	var login = new Login();
    	//每次让登录框出现即可
    	login.show();
    }
    

    上面的代码根据单例模式的使用构造函数来实现的。这样在一开始生成了一个对象,之后使用的都是同一个对象。

    总结

    单例模式是一种非常实用的模式,特别是懒性单例技术,在合适时候创建对象,并且只创建唯一一个,这样减少不必要的内存消耗。


    正在学习设计模式,不正确的地方欢迎拍砖指正。

  • 相关阅读:
    又一道简单的题
    atoi函数的使用(将字符串转换成整型数)
    【贪心】Radar Installation(POJ1328)
    【BFS】鸣人与佐助
    谍报分析
    适配器模式(C++实现)
    策略模式(C++)
    工厂模式(C++实现)
    桥接模式(C++实现)
    关于getMemory函数的几点思考
  • 原文地址:https://www.cnblogs.com/floatboy/p/singleton.html
Copyright © 2020-2023  润新知