先有问题再有答案
单例是什么?
全局变量和单例是一回事嘛?
export一个实例是单例嘛?
通过属性拦截防止实例重复创建就能实现单例嘛?
为什么我们要用单例?
如何实现一个单例?
前端开发需要用到单例嘛?
什么是单例
单例模式是一种创建型设计模式,它保证
了只有一个实例,并提供一个全局访问点,供外部访问这个单一实例。
全局对象是单例嘛?
javascript
class Single {
name = '';
constructor(name) {
this.name = name;
}
}
window.SingleTon = new Single('s1')
等号左边是实例的作用域也就是可访问性,这是 静态词法作用域 决定的.
单例描述的是等号右边,需要保证实例对象全局唯一。
这是一个全局对象 它实现了全局可访问性并且可以实现状态共享,但是这并不能保证实例唯一。
javascript
const s1 = new Single('s1');
const s2 = new Single('s1');
s1 === s2 // false
export一个实例是单例嘛?
javascript
// a.js
const s1 = new Single('?s1');
export const SingleTon = s1;
现在外部在使用时直接使用实例化后的SingleTon对象,我们在a.js模块并没有暴露Single类,外部无法访问Single 也不能new新对象,这样保证了实例唯一,那么这样实现是单例嘛?
这样依然不是...
虽然没有导出Single类,但是我们依然可以通过继承链 创建出新的Single实例。
javascript
import { SingleTon } from './a.js'
const s2 = new SingleTon.constructor('s1'); // 通过构造函数 再次创建出新的实例
s2 === SingleTon // false
静态属性创建拦截
javascript
class Single {
name = '';
constructor(name) {
this.name = name;
}
static getInstance() {
// 判断是否已经 new 过 1 个实例
if (!Single.instance) {
// 若这个唯一的实例不存在,那么先创建它
Single.instance = new Single('s1')
}
// 如果这个唯一的实例已经存在,则直接返回
return Single.instance
}
}
const s1 = Single.getInstance();
const s2 = Single.getInstance();
s1 === s2 // true
getInstance静态方法的作用就是创建Single实例。
在第一次调用getInstance时,它会检查Single类是否已经有一个名为instance的实例,如果没有,那么它会创建该实例。然后每次调用getInstance方法时,都会返回这个已经创建的唯一的实例。
javascript
Single.instance = null;
const s3 = Single.getInstance();
s1 === s3 // false
因为拦截的标识是在构造函数上的所以并不安全,可以被重新赋值。
symbol属性拦截
有的同学可能会说 使用symbol这样就不会被访问到了, 如下:
javascript
const instance = Symbol('instance')
static getInstance() {
// 判断是否已经 new 过 1 个实例
if (!Single[instance]) {
// 若这个唯一的实例不存在,那么先创建它
Single[instance] = new Single('s1')
}
// 如果这个唯一的实例已经存在,则直接返回
return Single[instance]
}
这种方式也是不对的。symbol 只是默认不能访问到 我们可以通过其他方式获取到。
javascript
const symbolSingle = Object.getOwnPropertySymbols(Single)[0];
Single[symbolSingle] = null;
const s2 = Single.getInstance();
s1 === s2 // false
单例应用场景
- 配置管理器:对于需要在多个地方使用的应用程序配置信息,可以将这些配置存放在单例对象中。这样可以避免跨文件传递配置,简化代码,并确保全局只有一份可编辑的配置数据。
- 数据库连接池:为了避免每次需要访问数据库时都创建一个新连接,可以使用一个单例对象保存数据库连接池。这使得程序可以复用已创建的连接,降低资源开销,提高性能。
- 日志记录器:在应用程序中,日志记录通常是一个全局使用的功能。将日志记录器封装为单例对象可以让全局的代码都方便地访问和使用它,同时避免了多次创建日志记录对象的资源浪费。
实现单例的方式
无论别人如何使用,都要保证实例唯一
, 想要实现真正的单例模式,我们首先需要知道js创建对象的本质是构造函数调用,所以我们需要拦截构造函数,且拦截的标识不能被外部更改。
函数式
javascript
const SingleTon = (function(){
let single;
function SingleObj (name){
if(!single){
single = this;
}
this.name = name;
return single;
}
return SingleObj
})();
const s1 = new SingleTon('s1');
const s2 = new SingleTon('s2');
const s3 = new s1.constructor('s3')
s1 === s2 // true
s1 === s3 // true
OOP
javascript
// A.js
let instance;
export class SingleTon {
name = '';
constructor(name) {
if (!instance) {
instance = this;
}
this.name = name;
return instance;
}
}
const s1 = new SingleTon('s1');
const s2 = new SingleTon('s2');
const s3 = new s1.constructor('s3')
s1 === s2 // true
s1 === s3 // true
在构造函数constructor中,如果instance不存在,那么就将当前实例 this 赋值给 instance,否则直接返回已经存在的 instance。这样保证了一个类只有一个实例。
通过模块化防止instance被外部访问到 可以防止标识被清空。
代理方式
使用 JavaScript 的 Proxy 来实现一个单例模式
javascript
function getSingleTon(className) {
let instance;
const p = new Proxy(className, {
construct(target, arg) {
if (!instance) {
instance = Reflect.construct(target, arg);
}
return instance;
},
});
p.prototype.constructor = p;
return p;
}
const SingleTon = getSingleTon(Single);
const s1 = new SingleTon();
const s2 = new SingleTon();
const s3 = new s1.constructor()
s1 === s2 // true
s1 === s2 // true
在处理程序对象中,定义了一个 construct
属性,它是一个函数,当代理对象被构造时调用。Reflect.construct
用于构造一个新对象,其行为类似于 new
操作符。
在 construct
函数中,首先检查 instance
是否已经存在。如果不存在,使用 Reflect.construct
构造一个新的实例,并将其存储在 instance
中。如果 instance
已经存在,就直接返回这个实例。
将代理对象的 prototype.constructor
属性设置为代理对象本身,这样通过 constructor
属性访问的构造函数也会指向代理对象。
实际业务中的应用
对于纯前端的项目 一般基于react/vue这些框架做开发 前端面临的主要功能是UI绘制 响应交互 与服务端,客户端通信等逻辑。一般通过es 模块化是可以达到和单例相同作用的。所以除非是框架设计或者是公共库开发,实际业务中并不常用...