【设计模式】单例模式
定义
什么是单例模式?
保证一个类仅有一个实例,并提供一个访问它的全局访问点
单例模式是最简单最基础的一种模式,对于一个类,就是我们无论使用什么方式,无论准备获取多少次,所获得的都是同一个实例,也就是一个类只能被实例化一次。
简单例子
class AClass {
//非静态属性 实例化后生成
public aClassname: string;
// 私有化构造器私有化,禁止外部new
private constructor(name: string) {
this.aClassname = name
}
// 实例化对象
private static instance: AClass = new AClass('init');
// 使用公有的静态方法,返回实例
public static getInstance(): AClass {
return this.instance;
}
}
console.log('AClass.getInstance().name', AClass.getInstance().aClassname)
//AClass.getInstance().name init
这个例子禁用了外部使用new关键字来声明获取实例,提供了一个静态方法来进行唯一实例访问。
懒汉与饿汉单例
根据类被实例化的时间,单例模式又被分为懒汉单例和饿汉单例。懒汉单例是指在第一次调用实例的时候实例化。饿汉单例是指在类加载的时候就实例化(第一次调用前实例化)
饿汉单例
饿汉单例是指在类加载的时候就实例化(第一次调用前实例化),前面的简单例子就是一个饿汉单例。
懒汉单例
懒汉单例也叫惰性单例,它在第一次调用实例的时候实例化,避免了内存的浪费。
class AClass {
//非静态属性 实例化后生成
public aClassname: string;
// 私有化构造器私有化,禁止外部new
private constructor(name: string) {
this.aClassname = name
}
// 实例化对象
private static instance: AClass | null = null;
// 使用公有的静态方法,返回实例
public static getInstance(): AClass {
if (this.instance === null) {
this.instance = new AClass('init');
}
// 存在实例则直接返回
return this.instance;
}
}
console.log('AClass.getInstance().name', AClass.getInstance().aClassname)
//AClass.getInstance().name init
使用new
前面的单例模式都禁用了外部调用构造函数,但是当我们提供一个单例类的时候,使用者可能并不知道调用哪个函数来获取实例,因此可以放出构造函数,使得用户可以像使用普通类一样,使用new关键字获取实例。
let c = new BClass();//Cannot access 'BClass' before initialization
const BClass = (function () {
let instance;
//闭包中将属性私有化
let _privateName = 'privateName'
let BClass = function () {
if (instance) {
return instance;
}
this.publicName = 'publicName'
return instance = this;
};
BClass.prototype.setPrivateName = function (name) {
_privateName = name;
};
BClass.prototype.getPrivateName = function () {
return _privateName
};
return BClass;
})();
let a = new BClass();
let b = new BClass();
console.log(a.publicName)//publicName
console.log(a.privateName)//undefined
// console.log(a.instance.privateName)//TypeError: Cannot read property 'privateName' of undefined
console.log(a.getPrivateName())//privateName
console.log(a === b)//true
a.setPrivateName('A')
console.log(b.getPrivateName())//A
上面这种写法是JavaScript中的写法,而如果想要在TS中实现,首先得考虑this的类型检查与构造签名的问题。
这种写法看似可以,但依旧有几个关键问题
- 自执行的匿名函数阅读起来很麻烦,并且其需要先自执行初始化的机制将导致其无法使用,这一点可以和下面这个例子进行简单对比
let c = new CClass('C');
console.log(c.name)//C
function CClass(name) {
this.name = name;
return this
};
- 另外,上面的私有属性privateName看似正常,但实际上它并不属于a,b实例,它存在于闭包作用域中,但是因为给BClass上添加了可以访问和设置privateName的两个方法,因此使得privateName看起来如同BClass的私有属性一般。当然,它确实有这个效果。
我们将它改写成TypeScript的格式
let c = new BClass();//声明之前已使用的块范围变量“BClass”。ts(2448)
const BClass = (function () {
let instance: any;
//闭包中将属性私有化
let _privateName = 'privateName'
let BClass = function (this: any) {
if (instance) {
return instance;
}
this.publicName = 'publicName'
return instance = this;
};
BClass.prototype.setPrivateName = function (name: string) {
_privateName = name;
};
BClass.prototype.getPrivateName = function () {
return _privateName
};
return BClass;
})() as any;
let a = new BClass();
let b = new BClass();
console.log(a.publicName)//publicName
console.log(a.privateName)//undefined
// console.log(a.instance.privateName)//TypeError: Cannot read property 'privateName' of undefined
console.log(a.getPrivateName())//privateName
console.log(a === b)//true
a.setPrivateName('A')
console.log(b.getPrivateName())//A
可以通用的单例
前面的单例都需要定制设计,但实际上大可不必,单例的威力并不止于此。唯一实例是单例的核心,抓住核心,完全可以有更加丰富的拓展。
将单例的管理提取出来,让它专注于单例对象管理
TypeScript
const SingleManager = function (Class: any) {
let result: any;
return function (this: any) {
if (result) return result
try {
class SingleClass extends Class {
constructor(...rest: any[]) {
super(...rest)
}
}
result = new SingleClass(...arguments)
} catch {
throw 'params is not Class'
}
return result;
}
} as any;
class DClass {
//私有属性 实例化后生成
private dClassname = 'init';
// 返回静态实例
constructor(name: string) {
this.dClassname = name
}
setDClassName(name: string) {
this.dClassname = name;
}
getDClassName(): string {
return this.dClassname;
}
}
const DClassPlus = SingleManager(DClass);
const a = new DClassPlus('A')
const b = new DClassPlus('B')
console.log(a.getDClassName())//A
console.log(b.getDClassName())//A
console.log(a.dClassname)//A
console.log(a === b)//true
这里出现了一个问题,a.dClassname居然可以被直接访问了,但在DClass中,这个属性却是私有属性
简化一下
const SingleManager = function (fn) {
let result;
return function () {
return result || (result = fn.apply(this, arguments));
}
};
const CClass = function (name) {
this.className = name
return this
}
const CClassPlus = SingleManager(CClass);
const c = new CClassPlus('Tom')
const d = new CClassPlus('Tony')
console.log(c.className)//Tom
console.log(d.className)//Tom
console.log(c === d)//true
和大多数缓存方式一样,用一个变量 result 来保存 fn 的计算结果,存在时直接返回之前的运算结果。lodash中的内置函数before就是一个经典的应用
lodash中before函数的源码
function before(n, func) {
var result;
//如果func不是函数,抛出异常
if (typeof func != 'function') {
throw new TypeError(FUNC_ERROR_TEXT);
}
n = toInteger(n);
//返回新的限定函数
return function() {
//如果当前调用次数小于限定次数,执行限定函数并使用result存放结果
if (--n > 0) {
result = func.apply(this, arguments);
}
if (n <= 1) {
func = undefined;
}
//返回最后一次调用的结果
return result;
};
}
再次扩展,使用单例的思想来进行请求缓存
const Service = {
fetchData() {
return new Promise((resolve) => {
setTimeout(() => resolve(Math.round(Math.random() * 10)), 3000)
})
}
}
const fetchData = SingleManager(Service.fetchData)
fetchData().then((res: any) => console.log('res1', res))//res1 9
fetchData().then((res: any) => console.log('res2', res))//res2 9
let c = fetchData()
let d = fetchData()
console.log(c === d)//true
setTimeout(() => console.log(c), 3000)//Promise { 9 }
使用单例管理器对请求接口进行包装后,无论后面调用了多少次请求接口,永远都只会返回第一次调用的数据,剩余的请求将被忽略。
这对于那些经常请求但是数据很少变动的接口而言是非常有用的,并且相对于使用浏览器缓存而言,这些请求数据都只存在于当前页面进程中,浏览器控制台中是无法获取的,页签关闭,这些数据便会被回收。
对上面的这个例子进行一定的修改
const SingleManager = function (fn: Function) {
let result: any;
let argArr: any[] = [];
return function (this: any, ...args: any[]) {
const flag = argArr.length !== args.length || args.filter((item, index) => item !== argArr[index]).length > 0
if (flag) {
argArr = args;
result = fn.apply(this, args)
}
return result || (result = fn.apply(this, args));
}
};
const Service = {
fetchData(n: number) {
return new Promise((resolve) => {
setTimeout(() => resolve(n), 3000)
})
}
}
const fetchData = SingleManager(Service.fetchData)
fetchData(1).then((res: any) => console.log('res1', res))//res1 1
fetchData(2).then((res: any) => console.log('res2', res))//res2 2
let c = fetchData(3)
let d = fetchData(3)
console.log(c === d)//true
c.then((res: any) => console.log('res3', res))//res3 3
d.then((res: any) => console.log('res4', res))//res4 3
这样就只有当参数不同的时候才会调用接口,参数不变的时候将直接使用之前的数据,当然,这里只是一个基本类型的判断,如果参数中具有复杂类型,那还需要更加深入的判断,当然,这种判断也可以提取出去,直接传入一个标识符用于控制是否调用被包装的函数。
上面的扩展已经不算是单例了,但它的思路和单例具有异曲同工之处,唯一性。