谈谈 JavaScript 单例模式及应用

2,035 阅读4分钟

单例模式,也叫单子模式,是一种常用的软件设计模式。在应用这个模式时,单例模式的类必须保证只有一个实例的存在,许多时候整个系统只需要拥有一个全局对象,这样有利于我们协调系统整体的行为。

实现单例

保证一个类仅有一个实例,并提供一个访问它的全局访问点,这是单例的核心思想。

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 登录浮窗为例:
image.png
图来自《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

image.png
image.png
全局变量 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 中单例模式的核心思想是 确保全局有且只有一个实例对象,并提供访问它的方法。

参考文章