JavaScript 中的单例模式
JavaScript 中的单例模式
大家好,
昨天我写了人生中的第一篇技术文章!内容是关于 JavaScript 中的单例设计模式。我把它发布到 Reddit 上,结果收到了一些关于这种模式本身的负面反馈。这篇文章纯粹是信息分享,我并没有说“用单例”或者“不用单例”。我只是说“你可以这样实现它”。至于你是否喜欢它,它是否适合你的应用,是否能解决你的问题,都取决于你自己。
另外,由于这是我创作的第一篇文章(不算 10 年前关于 jQuery 点击绑定的文章),我非常希望得到反馈,包括技术术语(我并不自诩为专家,可能在很多地方都错了)和语言方面。
JavaScript 中的单例模式
单例模式是编程中较为人熟知的模式之一。虽然有些人将其视为反模式,但了解一些相关知识仍然很有价值。
创建这样的类并不难,但有一些注意事项。首先,它的构造函数必须返回实例。其次,这样的类不能被任何祖先类继承或修改。一般来说,继承只会链接到初始实例。
那么,如何编写单例类呢?首先,我们像往常一样开始:
class SingletonClass {
constructor() {}
}
这是 ES2015 格式中的标准类命名法。请注意并记住这个名称。创建任何对象时,尤其是单例类,务必谨慎选择名称。我知道命名很困难,但在这里我们将使用名称而不是类名this。
第二步是在类中定义实例键。请注意,这是一个静态值,它指的是类本身,而不是类的实例。
class SingletonClass {
constructor() {
if (!!SingletonClass.instance) {
return SingletonClass.instance;
}
SingletonClass.instance = this;
return this;
}
}
我们来解释一下。constructor首先要检查是否SingletonClass.instance存在。为什么不存在呢this?就像我之前说的,我们指的是类本身。这是它的静态值,与实例无关。如果条件满足,则意味着该类之前已经创建过,可以返回旧实例,而无需创建新实例。
接下来,我们将实例赋值SingletonClass.instance给类this,这意味着我们将当前实例绑定到该类,从而将它们耦合起来。
最后,我们返回 `null` this。这可能会让人困惑,但请查阅相关资料。我们SingletonClass.instance之前已经返回过 `null`。即使没有这个 `null` return,代码也能运行,但保持方法返回值的一致性是一种良好的编程习惯。
好的,这一切都很好,但是我们如何证明创建新实例是不可能的呢?问得好。让我们给类添加一些功能,例如,让它返回我们给它起的名字(没错,这就是编程!)。
class SingletonClass {
constructor(name = "") {
if (!!SingletonClass.instance) {
return SingletonClass.instance;
}
SingletonClass.instance = this;
this.name = name;
return this;
}
getName() {
return this.name;
}
}
现在我们来创建一些实例:
const instanceOne = new SingletonClass("One");
const instanceTwo = new SingletonClass("Two");
const instanceThree = new SingletonClass();
好的,就这么简单。现在我们可以记录下来了:
console.log(`Name of instanceOne is "${instanceOne.getName()}"`);
console.log(`Name of instanceTwo is "${instanceTwo.getName()}"`);
console.log(`Name of instanceThree is "${instanceThree.getName()}"`);
你能猜到哪些人会登出吗?
实例一的名称为“一”;
实例二的名称为“一”;
实例三的名称为“一”。
为什么会这样?因为它是单例类!它总是使用最初创建的唯一实例。尝试改变顺序,把它移到instanceThree上面instanceOne。console.log现在这些代码是什么意思?
还有一点就是扩展。这是面向对象编程中一个非常流行的特性。流行,但也容易被误用、滥用等等。从技术上讲,单例模式是不能被扩展的,因为它们没有任何祖先。但是,眼见为实。让我们创建一个新类来扩展旧类:
class Extending extends SingletonClass {
shoutName() {
return this.name.toUpperCase();
}
}
所以,通常Extending应该有两个——一个getName是源自某个库的库SingletonClass,另一个shoutName是它自身的库。我们来看看:
const A = new Extending();
console.log("getName" in A);
console.log("shoutName" in A);
你在控制台中看到了什么?
真
假
为什么?因为实例是在我们定义它之前创建的instanceOne。扩展进程甚至无法启动,因为SingletonClass constructor它首先返回的是实例。
扩展单例类的唯一方法是在任何实例被初始化之前进行。但这是一种极端的反模式,因为你无法确定在你进行扩展之前,其他人不会使用基类。当然,你可以在声明之后立即扩展它,但是……为什么要这样做呢?
现在我们知道如何创建一个只有一个实例的类了。这有用吗?如果你想利用类的优势,但又不想让别人随意使用,那么单例模式就很有用。听起来有点讽刺,但并非如此。想想日志记录器。你为什么要创建多个日志记录器?你只需要一个,而且可以用单例模式来实现。再想想数据库缓存。你希望所有数据都可用,而无需考虑在其他地方共享状态。
完整的代码可以在我的CodePen上找到,其中还包含一个针对不信者的多重实例检查。
文章来源:https://dev.to/tomekbuszewski/singleton-in-javascript-1d5i