最近打算系统的学习javascript设计模式,以便本身在开发中遇到问题能够按照设计模式提供的思路进行封装,这样能够提升开发效率而且能够预先规避不少未知的问题。javascript
先从最基本的单例模式开始。html
单例模式,从名字拆分来看,单指的是一个,例是实例,意思是说屡次经过某个类创造出来实例始终只返回同一个实例,它限制一个类只能有一个实例。单例模式主要是为了解决对象的建立问题。单例模式的特色:java
一个类只有一个实例设计模式
对外提供惟一的访问接口浏览器
在一些以类为核心的语言中,例如java,每建立一个对象就必须先定义一个类,对象是从类建立而来。js是一门无类(class-free)的语言,在js中建立对象的方法很是简单,不须要先定义类便可建立对象。缓存
在js中,单例模式是一种常见的模式,例如浏览器中提供的window对象,处理数字的Math对象。闭包
在js中实现单例最简单的方式是建立对象字面量,字面量对象中能够包含多个属性和方法。app
var mySingleton = { attr1:1, attr2:2, method:function (){ console.log("method"); } }
以上建立一个对象,放在全局中,就能够在任何地方访问,要访问对象中的属性和方法,必须经过mySingleton这个对象,也就是说提供了惟一一个访问接口。dom
扩展mySingleton对象,添加私有的属性和方法,使用闭包的形式在其内部封装变量和函数声明,只暴露公共成员和方法。函数
var mySingleton = (function (){ //私有变量 var privateVal = '我是私有变量'; //私有函数 function privateFunc(){ console.log('我是私有函数'); } return { attr1:1, attr2:2, method:function (){ console.log("method"); privateFunc(); } } })()
把privateVal和privateVal被封装在闭包产生的做用域中,外界访问不到这两个变量,这避免了对全局命名污染。
不管使用对象字面量或者闭包私有化的方式建立单例,都是在脚本一加载就被建立。有时候页面可能不会用到这个单例对象,这样就会形成资源浪费。对于这种状况,最佳处理方式是使用惰性单例,也就是在须要这个单例对象时再初始化。
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()后获得的是同一个对象,这样经过一个函数不会建立多个对象,起到节省资源的目的。
能够使用构造函数的方式,创造单例对象:
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调用构造函数建立实例时,又会从新建立新的对象,那么p1和p2就不是同一个对象,违反了单例模式一个类只能有一个实例。
不使用函数的静态属性缓存实例,而是从新改写构造函数:
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调用函数后,利用闭包的特性,返回了缓存的对象,因此p1和p2是同一个对象。
这样虽然也能够保证一个类只返回一个实例,但注意,第二次再次使用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
以上代码主要作了如下几件事:
改写mySingleton函数为匿名函数
改写mySingleton的原型为第一次经过new建立的实例
由于改写了prototype,要把constructor指回mySingleton
显式返回经过改写后mySingleton构造函数构造出的实例
不管使用多少次new调用mySingleton这个构造函数,都返回同一个对象,而且这些对象都共享同一个原型。
根据上述,在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 = {};
使用面向对象实现一个登陆框,在点击登陆按钮后登陆框被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(); }
上面的代码根据单例模式的使用构造函数来实现的。这样在一开始生成了一个对象,以后使用的都是同一个对象。
单例模式是一种很是实用的模式,特别是懒性单例技术,在合适时候建立对象,而且只建立惟一一个,这样减小没必要要的内存消耗。
正在学习设计模式,不正确的地方欢迎拍砖指正。