关键词:闭包 | ESM | CJS | cookie | ...
单例模式:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
在我以前做过的项目中,单例模式是一个非常常用的设计模式,我以前写过一些 C++ 的项目,其中自己封装的 DataManager 层,用于管理所有的数据,提供统一存储访问的API,这里就是一个单例模式,因为数据管理,有这个层所存在的唯一性。
很多人可能会觉得,我用 react 或者 vue 为什么没有这种感觉,感觉界面组件,都是可以多个实例的?而且有数据直接调用 API 接口不就行了吗?DataBase本身不就是一个单例吗?
其实这些说法没有错,但是实际上单例的需求很多,比如每个 window 对应的唯一的 Cookie ,比如在runtime时的 redux / vuex 等全局的 Data store ,无不充斥的单例的思想,但是这些是怎么实现的呢?让我们一一来解答。
Java 中的单例模式
public class SingleObject {
//创建 SingleObject 的一个对象
private static SingleObject instance = new SingleObject();
//让构造函数为 private,这样该类就不会被实例化
private SingleObject(){}
//获取唯一可用的对象
public static SingleObject getInstance(){
return instance;
}
public void showMessage(){
System.out.println("Hello World!");
}
}
以上的代码和图,是从菜鸟教程上拷贝下来的,我们可以看到,在 SingleObject 类中,定义了一个静态方法和函数,让我们通过 SingleObjectobject = SingleObject.getInstance(); 直接访问这个实例时,始终拿到的是同一个东西。
因为在 Java 中,static 所定义的内容,会被所有的对象共享使用。这种写法,在 es6 中同样有此定义。即直接在类上的属性,不用通过实例化即可获得。
当然,Java 的单例模式没有这么简单,其中懒汉、饿汉,以及各种情况下的 锁
的使用,都值得深入学习,这里我们不再多探究。
JavaScript 的实现
基于 Java 思想的单例
const SingletonPerson = function(name, age) {
this.name = name;
this.age = age;
}
SingletonPerson.getInstance = function(name, age) {
if(!this.instance) {
this.instance = new SingletonPerson(name, age);
}
return this.instance;
}
这样的话,对标上述的Java的例子,就是一个标准的单例,在函数SingletonPerson 上,挂载了一个函数 getInstance ,拿到的值永远都也是挂载在 SingletonPerson 这个函数上的 instance
\
但是这里存在几个问题:
- 这个实例其实是挂载在
SingletonPerson这个函数上的instance上的,对于我们的理解,可能会有一定的问题; - 我们的创建方法,需要显示地调用
getInstance这个函数。
\
其实说白了,我们就是要把这个 instance 给存储起来。然而,JS中的闭包特性,天然地支持这种存储局部变量的写法,我们来看一下。
一个普通的 JS 单例
const getInstance = (function(){
let instance = {
name: "luyi"
};
return function() {
return instance;
}
})();
const a = getInstance();
const b = getInstance();
console.log( a === b );
我们先看一下这段代码,通过闭包的特性,显然,我们拿到的 instance,永远是闭包中定义的变量,但是很多时候,我们的 getInstance 是需要初始化内容的,我们改造一下:
一个透明的单例
const SingletonPerson = (function() {
const Person = function(name, age) {
this.name = name;
this.age = age;
}
let instance = null;
return function(name, age) {
if(!instance) {
instance = new Person(name, age);
}
return instance;
}
})();
const a = new SingletonPerson("gavin",18);
const b = new SingletonPerson("luyi",19);
console.log(b, a===b); //Person {name: 'gavin', age: 18} , true
但是这样做了以后,也有一个缺点,就是 Person 的声明,放在了 闭包中,这其实是没必要的;
那么,把 Person 提出来以后,我们就可以做更多的事情,比如,把 SingletonPerson 函数,完全 singlelify 。
一个单例化的函数
const Person = function (name, age) {
this.name = name;
this.age = age;
}
const singlelify = function (Func) {
let instance = null;
return function (...args) {
if (!instance) {
instance = new Func(...args);
}
return instance;
}
};
const SigletonPerson = singlelify(Person);
const a = SigletonPerson("gavin", 20);
const b = SigletonPerson("luyi", 19);
console.log(b, a === b) //Person {name: 'gavin', age: 20}, true
模块化中的单例与闭包思想
cjs 中的单例体现
我们定义两个文件:
// single.js
let count = 0;
function getCount() {
return count++;
}
module.exports = {getCount};
// index.js
const a = require('./single').getCount();
const b = require('./single').getCount();
const c = require('./single').getCount();
console.log(a, b, c)
// 这里打印的结果是,0,1,2
据此,我们至少可以知道 count 变量的定义,是一个闭包,也就是说,这里的 a,b,c 的打印值,始终是同一个 count;
为什么呢?我们使用 webpack 命令打包一下:
./node_modules/.bin/webpack --entry ./index.js -o output
通过上面的红框我们可以看到,这里的定义的 count,变成了一个闭包中的 o 变量。
所以,这里如果要实现单例,我们可以直接对这两个文件这样改动:
// single.js
let instance = null;
function Person(name, age) {
this.name= name;
this.age = age;
}
function getInstance(name, age) {
if(!instance) {
instance = new Person(name, age);
}
return instance
}
module.exports = {getInstance};
// index.js
const a = require('./single').getInstance("luyi", 18);
const b = require('./single').getInstance("gavin", 19);
const c = require('./single').getInstance("candy", 20);
console.log(a, b, c, a===c)
// 这里打印的结果是,
// Person { name: 'luyi', age: 18 }
// Person { name: 'luyi', age: 18 }
// Person { name: 'luyi', age: 18 }
// true
讲完了 cjs , 我们再看看 ESM :
esm 中的单例体现
我们增加两个文件,构成这样的一个引用关系:
代码如下:
// single.js
function Person(name, age) {
this.name= name;
this.age = age;
}
let _instance = new Person("luyi", 19);
export const instance = _instance;
// moduleA.js
import { instance } from "./single";
instance.age = 30;
export const instanceA = instance;
// moduleB.js
import { instance } from "./single";
instance.name = "gavin"
export const instanceB = instance;
// index.js
import { instanceA } from "./moduleA";
import { instanceB } from "./moduleB";
console.log(instanceA,instanceB,instanceA === instanceB);
我们接下来还是用之前的 webpack 命令构建,然后执行,打印结果如下:
很明显,我们看到的是,esm 在导出时的传值,是一个引用的关系。
总结
单例本身不是一个特别复杂的设计模式,我见过很多种在 cjs 或者 esm 中的过度设计:把它封装成一个非常完善的单例模式;
但在实际应用中,我们只要掌握好 cjs 或者 esm 的模块化规则,对于一个唯一的持久化变量的定义,或许有更适合的方式。