定义

什么是单例模式?

保证一个类仅有一个实例,并提供一个访问它的全局访问点

单例模式是最简单最基础的一种模式,对于一个类,就是我们无论使用什么方式,无论准备获取多少次,所获得的都是同一个实例,也就是一个类只能被实例化一次。

简单例子

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

这样就只有当参数不同的时候才会调用接口,参数不变的时候将直接使用之前的数据,当然,这里只是一个基本类型的判断,如果参数中具有复杂类型,那还需要更加深入的判断,当然,这种判断也可以提取出去,直接传入一个标识符用于控制是否调用被包装的函数。

上面的扩展已经不算是单例了,但它的思路和单例具有异曲同工之处,唯一性。