手把手带你理解单例模式的实现思路,其实也就那样

200 阅读5分钟

这是我参与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 是什么呢?我们看下输出结果:

image.png

因为函数是被独立调用的。所以上面的 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);

输出结果:

image.png

第三版

我们看下第二版的代码,发现没有:

构造函数的代码跟 getInstance 函数,是独立的,我们能不能将把这个方法,统一到我们的构造函数当中。

image.png

实例的属性和原型上的方法,这两种是不行的。

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);

我们运行下上面的代码,看下输出结果:

image.png

实现了效果,但是如果我没有使用 new 关键字呢?那就是有 bug,我们来看看:

将上面的代码的调用方式,改成这样,测试下:

const s1 = Singleton('张三');
const s2 = Singleton('李四');
console.log(s1);
console.log(s2);
console.log(s1 === s2);

输出结果: image.png 你会发现,没有使用 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 的方式的话,那它实际上是将单例的功能和实例的功能,给它放在一起。

所以说我们并不希望这么做,我们根据单一职责原则。

一个函数只能做一件事情。那么现在你很明显是再做两件事情。 并不是做单独的事情。

如果是“只能实例化一次”的话,我们可以通过上面这种方式,让它只能实例化一次,返回的,都是同一对象,但是他的功能是耦合的,所以这种写法不推荐大家使用。

写到最后

单例的本质,实际上就是通过一个变量来存储我们的实例,通过判断当前的变量是否有值,来决定是否返回实例本身。

文中如有错误,欢迎在评论区指正,如果这篇文章帮助到了你或者喜欢,欢迎点赞和关注。