1 单例模式定义
单例模式是指一个类只有一个实例,并且提供一个能够访问它的全局访问点。例如缓存池,全局缓存,浏览器中的window对象。
2 单例模式的实现
就是使用一个变量标识来判断当前是否为否个类创建了一个实例,如果已创建则返回,没有创建则立即创建并保留在实例对象中返回。
var Singleton = function (name) {
this.name = name;
this.instance = null;
}
Singleton.getInstance = function(name) {
if (!this.instance) {
this.instance = new Singleton(name);
}
return this.instance;
}
var a = Singleton.getInstance('a');
var b = Singleton.getInstance('b');
console.log(a === b);
复制代码
使用闭包的方式完成上面的例子单例模式
var Singleton = function (name) {
this.name = name;
}
Singleton.getInstance = (function() {
var instance = null;
return function(name) {
if (!instance) {
instance = new Singleton(name);
}
return instance;
}
})();
var a = Singleton.getInstance('a');
var b = Singleton.getInstance('b');
console.log(a === b);
复制代码
3 透明的单例模式
实现一个透明
的单例类,可以让我们在使用的时候,与其他创建任何普通类一样。下例子的缺点:为了将instance封装起来,我们利用了自执行的匿名函数与闭包,并让这个匿名函数返回真正的Singleton的构造方法。
var CreateDiv = (function() {
var instance = null;
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.innnerHTML = this.html;
document.body.appendChild(div);
}
return CreateDiv;
})();
var a = new CreateDiv('X');
var b = new CreateDiv('Y');
console.log(a === b);
复制代码
4 用代理方式实现单例模式
现在来改造上面的例子(其实我理解的是:闭包 + 构造函数组合,闭包这里就是代理类)
- 移动普通函数到外面
- 创建代理类
function CreateDiv(html) {
this.html = html;
this.init();
}
CreateDiv.prototype.init = function() {
var div = document.createElement('div');
div.innnerHTML = this.html;
document.body.appendChild(div);
}
// 引用代理类
var SingletonProxy = (function() {
var instance = null;
return function (html) {
if (!instance) {
instance = new CreateDiv(html);
}
return instance;
}
})();
var a = new SingletonProxy('X');
var b = new SingletonProxy('Y');
console.log(a === b); // true
复制代码
5 javascript中的单例模式
上面都是模拟面向对象语言中的实现,单例对象是从类
中创建出来的。但是javascript是一门无类语言,因此在javascript中创建对象的方法非常简单,既然我们只要一个唯一的对象,何必创建类呢?传统的单例模式在javascript中并不适用。 单例模式是保证全局中只有一个实例,并提供全局访问。全局变量并不是单例模式,但是在javascript中经常把全局变量当作单例模式来进行使用。例如下面的例子:a对象时独一无二的。它满足只有一个实例,并且能够全局访问的条件。
var a = {};
复制代码
但是全局变量有很多问题,容易造成命名控件的污染。在程序中,不小心也会被变量覆盖。可以通过如下方式降低全局变量的命名污染。
- 使用命名空间 最简单的方式就是使用字面量的方式创建。将a, 定义在namespace1中,减少了变量和全局作用域打交道的机会。
var namespace1 = {
a: function() {
},
b: function() {
}
};
复制代码
- 使用闭包封装私有变量 使用比较封装将一些变量封装在闭包内部,只暴露出一些接口和外部通信。私有变量使用__开头,它们被封装在闭包产生的作用域中,外部访问不了这两个变量,这样避免了全局的命令污染。
var user = (function() {
var __name = 'yezi';
var __age = '10';
return {
getName: function() {
return __name;
}
}
})();
复制代码
6 惰性单例模式
例如在很多网站的右上角都会有一个登录或者注册按钮,点击后界面会出现一个弹出框,该弹出框应该只有一个,而这就适合使用单例模式。我们可以选择两种方式:
- 页面加载的时候,创建好弹出框,并隐藏。在使用的时候显示,实现了单例模型,整个页面只有一个Dialog。但是浪费了内存,产生了不必要的消费。
- 当使用的时候再创建弹出框,避免浪费资源内存(因为你可能并不会使用到该弹出框)。避免了不必要的消费,但是没有实现单一原则
var createDialog = function(loginTitle) {
var loginDialog = document.createElement('div');
loginDialog.id = 'loginDialog';
loginDialog.value = loginTitle;
loginDialog.style.display = false;
document.appendChild(loginDialog);
}
// 加载页面则从创建
createDialog();
document.getElmentById('login').onClick = function() {
document.getElementById('loginDialog').style.display = true;
};
// 点击登陆按钮再加载
document.getElmentById('login').onClick = function() {
createDialog();
document.getElementById('loginDialog').style.display = true;
};
复制代码
上面的问题已经出现了,那么我们如何实现满足两个条件的情况呢?下面我们利用闭包,封装了一个单例对象loginDialog,当在页面点击登录按钮,就生成loginDialog(存在直接返回,不存在则创建)
var createLoginDialog = (function() {
var loginDialog = null;
return function(loginTitle) {
if (!loginDialog) {
loginDialog = document.createElement('div');
loginDialog.id = 'loginDialog';
loginDialog.value = loginTitle;
loginDialog.style.display = false;
document.appendChild(loginDialog);
}
return loginDialog;
}
})();
document.getElmentById('login').onClick = function() {
createLoginDialog('loginTitle');
document.getElementById('loginDialog').style.display = true;
};
复制代码
上面我们已经实现了延迟创建以及单例模式。但是假如我们页面有注册dialog,客户沟通dialog,那么我们就需要创建很多个类似的闭包createRegisterDialog, createCilentDialog。这样显然是不合理的。我们可以把创建实例对象的职责(createLogin)与管理单例的职责(createSingleton:抽出不变的创建单例对象函数)分别放置到两个方法。
- createSingleton:不变的,管理单例职责。创建单例的函数作为参数,只生成一个对象
- createLogin || createRegister: 可变的,创建对象函数
var createSingleton = function(typeFunc) {
var intance = null;
return function(title) {
if (!instance) {
var instance = typeFunc.call(this, title);
}
return instance;
}
}
var createLogin = function(loginTitle) {
var loginDialog = document.createElement('div');
loginDialog.id = 'loginDialog';
loginDialog.value = loginTitle;
loginDialog.style.display = false;
document.appendChild(loginDialog);
return loginDialog;
}
var createRegister = function(registerTitle) {
var registerDialog = document.createElement('div');
registerDialog.id = 'registerDialog';
registerDialog.value = registerTitle;
registerDialog.style.display = false;
document.appendChild(registerDialog);
return registerDialog;
}
// 点击登陆按钮创建单例对象
var createLoginSingleton = createSingleton(createLogin);
document.getElmentById('login').onClick = function() {
createLoginSingleton('loginTitle');
document.getElementById('loginDialog').style.display = true;
};
// 点击注册按钮单例对象
var createRegisterSingleton = createSingleton(createRegister);
document.getElmentById('register').onClick = function() {
createRegisterSingleton('registerTitle');
document.getElementById('registerDialog').style.display = true;
};
复制代码
这种单例模式的用途不仅仅局限于创建对象。例如当页面得到ajax请求数据渲染列表完毕后,需要对一个列表添加注册事件。通过单例模式只需要绑定一次。
// 不变的单例管理函数
var getSingleton = function(func) {
var instance = null;
return function() {
return instance || (instance = func.apply(this, arguments));
}
}
// 可变的单例构造函数(操作)
var addListener = function() {
document.getElementById('addEl').addEventListener('click', function() {
console.log('add click');
});
return true;
}
// 获取单例对象
var addListenerSingle = getSingleton(addListener);
// 调用
var render = function() {
// 页面上一个按钮,点击后创建一个按钮,并注册事件(不敢点击多少次只会注册一个事件)
document.getElementById('cl').onclick = function() {
var a = document.createElement('button');
a.id = 'addEl';
a.innerHTML = 'A';
document.body.appendChild(a);
addListenerSingle();
};
}
复制代码