单例模式,也叫单子模式,是一种常用的软件设计模式。在应用这个模式时,单例模式的类必须保证只有一个实例的存在,许多时候整个系统只需要拥有一个全局对象,这样有利于我们协调系统整体的行为。
实现单例
保证一个类仅有一个实例,并提供一个访问它的全局访问点,这是单例的核心思想。
var instance
if (!instance) {
instance = singleInstance
}
基于上面代码的思路,实现一个简单的单例模式。
var Cache = function (options = {}) {
this.options = options;
};
Cache.prototype.get = function () {};
Cache.prototype.set = function () {};
var ProxySingletonCreateDiv = (function () {
var instance;
return function (options) {
if (!instance) {
instance = new Cache(options);
}
return instance;
};
})();
var a = new ProxySingletonCreateDiv();
var b = new ProxySingletonCreateDiv();
Cache 类和 ProxySingletonCreateDiv 类结合组成单例模式,instance 变量保存在 ProxySingletonCreateDiv 函数作用域的闭包环境中,用这个变量标识当前是否已创建过实例,如果是,则在下次获取时,直接返回已创建的实例。
JavaScript 中的单例
JavaScript 是一门无类(class-free)语言。传统的单例模式实现在 JavaScript 中并不适用。单例模式的核心是确保只有一个实例,并提供全局访问。
全局作用域中的全局变量不是单例模式,全局变量会存在很大的隐患,随着项目的体积和功能日益增大,很容易出现命名冲突、作用域内变量污染和变量覆盖等问题,给开发人员带来很多苦恼。
所以要减少全局变量使用,即使用全局变量也要将污染降到最低。
1) 命名空间
命名空间可以减少全局变量的数量,可以使用对象字面量将这一类的变量作为它的属性进行访问。
var namespace1 = {
a: function(){
alert (1);
},
b: function(){
alert (2);
}
};
2) 使用闭包封装私有变量
使用 IIFI 立即执行函数表达式,让 JS 编译器不在认为这是一个函数声明,而是立即执行函数,将私有变量保存在它的闭包环境中,暴露可以访问私有变量的接口。类似创建一个块级作用域,和其他作用域的变量隔离。
var user = (function () {
var __name = 'zhangsan';
var __age = 24;
return {
getUserInfo() {
return __name + '-' + __age;
},
setUserInfo(str) {
var [name, age] = str.split('-');
__name = name;
__age = age;
},
};
})();
惰性单例
惰性单例指的是在需要的时候才创建对象实例。就像第一个例子中,只有在 new ProxySingletonCreateDiv() 时才创建单例对象。
以web QQ 登录浮窗为例:
图来自《JavaScript 设计模式与实践》
只有用户点击登录时,才创建一个全局且唯一的登录弹窗,而不是页面加载完成后就创建弹窗再隐藏。可以使用单例模式创建弹窗,保证登录弹窗全局只有唯一一个。
var createLoginLayer = (function(){
var div;
return function(){
if ( !div ){
div = document.createElement( 'div' );
div.innerHTML = '我是登录浮窗';
div.style.display = 'none';
document.body.appendChild( div );
}
return div;
}
})();
document.getElementById( 'loginBtn' ).onclick = function(){
var loginLayer = createLoginLayer();
loginLayer.style.display = 'block';
};
当下次需要创建 iframe 或 script 标签时,又要重新抄一份重复的代码修改,这是不能接受的。因此,把管理单例的逻辑从代码中抽离出来,用一个变量标识是否创建过对象,如果是,在下次直接返回已创建的对象。
通用的单例模式代码:
var getSingle = function( fn ){
var result;
return function(){
return result || ( result = fn .apply(this, arguments ) );
}
};
把创建单例的职责和管理单例的职责分别放在两个方法里,这两个方法可以独立变化,互不影响。当它们连接在一起的时候,就完成了创建唯一单例的功能。
创建登录弹窗:
var createLoginLayer = function(){
var div = document.createElement( 'div' );
div.innerHTML = '我是登录浮窗';
div.style.display = 'none';
document.body.appendChild( div );
return div;
};
var createSingleLoginLayer = getSingle( createLoginLayer );
document.getElementById( 'loginBtn' ).onclick = function(){
var loginLayer = createSingleLoginLayer();
loginLayer.style.display = 'block';
};
创建 iframe:
var createSingleIframe = getSingle( function(){
var iframe = document.createElement ( 'iframe' );
document.body.appendChild( iframe );
return iframe;
}); ;
document.getElementById( 'loginBtn' ).onclick = function(){
var loginLayer = createSingleIframe();
loginLayer.src = 'http://baidu.com';
};
更多单例模式场景
vuex
全局变量 Vue 在 Vuex 首次注册时保存 Vue 的构造器,当重复注册的时候即抛出错误提示。
我项目中使用单例的场景
使用 uni-app 开发外卖商城时,实现了一个缓存器的类,可以将指定的数据写入缓存或内存。即可以在 Vue 组件中通过 this.$xxx.get(key) 获取数据,也希望在任何 *.js 文件中引入这个缓存器通过 xxx.get(key) 获取同一个缓存器下的数据,我选择使用单例模式,把创建单例 Cache 类和管理单例的 getSingle 组合成单例模式。
getSingle 管理已创建的实例。多次访问时,如果实例已创建,则直接返回已创建的缓存器实例。
目录:
cache
- cache.js
- index.js
cache.js
import { getSingle } from "@/utils/index";
/* 省略其他代码 */
/* 省略缓存器的实现 */
class Cache {
constructor(timeout) {}
set(name, data, timeout = timeoutDefault) {}
get(name) {}
delete(name) {}
has(name) {}
clear() {}
}
/**
* 单例模式
* 共享同一个实例对象
*/
const ProxyCache = getSingle((...rest) => {
return new Cache(...rest);
});
export default ProxyCache;
_cache入口文件 index.js: _
import cache from "./cache";
export default {
install(Vue, { timeout = 0 } = {}) {
Vue.prototype.$cache = cache(timeout);
},
/**
* 非 .Vue 文件通过 Storage 函数获取
* @param timeout
* @returns {Cache}
* @constructor
*/
getInstance({ timeout = 0 } = {}) {
return cache(timeout);
},
};
_main.js: _
import Vue from "vue"
import cache from "./cache"
Vue.use(cache)
当 Vue.use(cache) 注册时,就已将 Cache 类实例初始化,通过 getSingle 函数管理已创建的缓存器实例。当在其他文件通过 getInstance() 获取时,则返回已创建的缓存器实例,达到全局只有一个实例的目的。
*.vue 文件中访问
this.$cache // 缓存器实例
*.js 文件访问
import Cache from "@/cache/index.js"
const cache = Cache.getInstance() // 缓存器实例
以上两种访问的都是同一个 Cache 实例对象,所以,存取数据都是一样的。
总结
JavaScript 单例不像传统有类的语言,在 JavaScript 中单例模式的核心思想是 确保全局有且只有一个实例对象,并提供访问它的方法。