这是我参与8月更文挑战的第21天,活动详情查看8月更文挑战
前言
你有没有思考过这样一个问题:如何才能保证一个类仅有一个实例?这就要说到我们的单例设计模式啦。今天我们来讲讲单例模式。
什么是单例模式?
保证一个类仅有一个实例,并提供一个访问它的全局访问点,这样的模式就叫做单例模式
单例模式,就是划分命名空间,并将属性和方法组织在一起的一种方式,只是实例化一次,每次返回的实例都是同一对象。
单例模式实现思路
一般情况下,当我们实现一个构造函数,通过 new 关键字调用构造函数来生成任意多的实例对象。像这样:
function Singleton(name) {
this.name = name;
}
const s1 = new Singleton('张三');
const s2 = new Singleton('李四');
console.log(s1 === s2);
运行上面的代码,你会发现,输出结果是 false,因为 s1 和 s2 是没有关系,两者是相互独立的对象,各占一块内存空间。而单例模式想要做到的是:
不管我们尝试去创建多少次,它都只给你返回第一次所创建的那唯一的一个实例。
现在我们就是要实现“一个类仅有一个实例”,我们来尝试实现一下:
第一版
通过 getInstance 函数做处理,当 this.instance 不存在的话,则创建实例,否则,则直接返回实例。
function Singleton(name) {
this.name = name;
}
Singleton.prototype.getName = function () {
console.log(this.name);
}
function getInstance(name) {
console.log('this', this); // 打印 this
if (!this.instance) {
this.instance = new Singleton(name);
}
return this.instance;
}
const s1 = getInstance('张三');
const s2 = getInstance('李四');
console.log(s1);
console.log(s2);
console.log(s1 === s2);
上面这个代码,可以实现效果,不知道大家有没有注意到上面代码,有打印 this,那这个 this 是什么呢?我们看下输出结果:
因为函数是被独立调用的。所以上面的 this 指向 window,相当于绑定了全局对象,可以在控制台手动修改,所以会导致结果不可信。我们要基于上面代码,进行优化。
第二版
第一版代码,存在 this 指向 window 的问题,我们可以通过闭包的方式。能够将全局的对象变成闭包中的局部变量。
原来的话,我们通过在 window 上面绑定一个全局变量,现在的话,闭包的方式,绑定了一个局部变量。它都等于我们当前的实例。
那么也就说单例的本质,实际上就是通过一个变量来存储我们的实例,通过判断当前的变量是否有值,来决定是否返回实例本身。
function Singleton(name) {
this.name = name;
}
Singleton.prototype.getName = function () {
console.log(this.name);
}
// 优化的地方
let getInstance = (function(){
var instance;
return function(name) {
if (!instance) {
instance = new Singleton(name);
}
return instance;
}
})();
const s1 = getInstance('张三');
const s2 = getInstance('李四');
console.log(s1);
console.log(s2);
console.log(s1 === s2);
输出结果:
第三版
我们看下第二版的代码,发现没有:
构造函数的代码跟 getInstance 函数,是独立的,我们能不能将把这个方法,统一到我们的构造函数当中。
实例的属性和原型上的方法,这两种是不行的。
getInstance 这个方法类似于一个工具函数,构造函数本质是对象,我们可以将 getInstance 绑定在对象上面,那么它就是静态方法。
所以我们再来优化下:
其实主要就是将 getInstance 方法放到 Singleton 构造函数上面,修改了以后,调用的方式也需要改变了。从之前的 getInstance 改成 Singleton.getInstance。
function Singleton(name) {
this.name = name;
}
Singleton.prototype.getName = function () {
console.log(this.name);
}
// 改动
Singleton.getInstance = (function(){
var instance;
return function(name) {
if (!instance) {
instance = new Singleton(name);
}
return instance;
}
})();
// 改动
const s1 = Singleton.getInstance('张三');
const s2 = Singleton.getInstance('李四');
console.log(s1);
console.log(s2);
console.log(s1 === s2);
第四版
我们来看看第三版的代码,执行 Singleton.getInstance('张三') 这个代码的时候,this 的指向是什么?
this 当然是 Singleton,既然 this 是构造函数的,我们就没有必要单独声明一个局部变量。
我们再修改下代码,有没有发现,有跟第一版的代码,有点相似,但是有不同的地方,留意下。
function Singleton(name) {
this.name = name;
}
Singleton.prototype.getName = function () {
console.log(this.name);
}
Singleton.getInstance = function(name) {
if (!this.instance) {
this.instance = new Singleton(name);
}
return this.instance;
}
const s1 = Singleton.getInstance('张三');
const s2 = Singleton.getInstance('李四');
console.log(s1);
console.log(s2);
console.log(s1 === s2);
提个要求
要求:当前只能够通过 new 的方式,实现一次。也就是我不希望你对象调用。
我们修改下 Singleton 构造函数和调用方式需要改成 new 的方式,需要将当前实例,返回的给 flag 本身,进行存储
let Singleton = (function() {
var flag;
return function(name) {
if (!flag) {
this.name = name;
flag = this;
}
return flag;
}
})();
Singleton.prototype.getName = function () {
console.log(this.name);
}
const s1 = new Singleton('张三');
const s2 = new Singleton('李四');
console.log(s1);
console.log(s2);
console.log(s1 === s2);
我们运行下上面的代码,看下输出结果:
实现了效果,但是如果我没有使用 new 关键字呢?那就是有 bug,我们来看看:
将上面的代码的调用方式,改成这样,测试下:
const s1 = Singleton('张三');
const s2 = Singleton('李四');
console.log(s1);
console.log(s2);
console.log(s1 === s2);
输出结果:
你会发现,没有使用 new 的情况,this 相当于 window,所以我们还需要修改下代码。
要保证是 new 执行。如果你不使用 new 执行的话,就会报错“必须使用 new 来调用构造函数”
let Singleton = (function() {
var flag;
return function(name) {
if (!flag) {
// 确认函数被调用时没有使用 new
if (new.target !== undefined) {
this.name = name;
flag = this;
} else {
throw new Error('必须使用 new 来调用构造函数');
}
}
return flag;
}
})();
Singleton.prototype.getName = function () {
console.log(this.name);
}
const s1 = new Singleton('张三');
const s2 = new Singleton('李四');
console.log(s1);
console.log(s2);
console.log(s1 === s2);
我们来分析下:
如果直接通过 new 的方式的话,那它实际上是将单例的功能和实例的功能,给它放在一起。
所以说我们并不希望这么做,我们根据单一职责原则。
一个函数只能做一件事情。那么现在你很明显是再做两件事情。 并不是做单独的事情。
如果是“只能实例化一次”的话,我们可以通过上面这种方式,让它只能实例化一次,返回的,都是同一对象,但是他的功能是耦合的,所以这种写法不推荐大家使用。
写到最后
单例的本质,实际上就是通过一个变量来存储我们的实例,通过判断当前的变量是否有值,来决定是否返回实例本身。
文中如有错误,欢迎在评论区指正,如果这篇文章帮助到了你或者喜欢,欢迎点赞和关注。