[单例模式]- ESM / CommonJS 规范

·  阅读 95

关键词:闭包 | 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 的模块化规则,对于一个唯一的持久化变量的定义,或许有更适合的方式。

参考资料

单例模式-菜鸟教程

JavaScript设计模式与开发实践

分类:
前端
标签:
分类:
前端
标签: