在本文中,我们将探讨在JavaScript中实现单例的最佳方法,并研究随着ES6的兴起它是如何发展的。
在大量生产中使用的语言中,JavaScript的发展速度最快,它看起来更像最早的迭代,而更像Python,而ECMA International提出了每一项新规范。 尽管这些更改有很多不利因素,但新的JavaScript确实成功地使代码易于阅读和推理,并以遵循软件工程最佳实践(特别是模块化和SOLID原理的概念)的方式更易于编写,并且更容易组装成规范的软件设计模式。
解释ES6
自2009年对ES5进行标准化以来,ES6(即ES2015)是该语言的第一个主要更新。 几乎所有现代浏览器都支持ES6 。 但是,如果需要使用较旧的浏览器,则可以使用Babel等工具将ES6代码轻松地转换为ES5。 ES6为JavaScript提供了许多新功能,包括用于类的高级语法和用于变量声明的新关键字 。 您可以通过阅读有关该主题的SitePoint文章来了解更多信息。
什么是单例
如果您不熟悉单例模式 ,则其核心是一种设计模式,它将类的实例化限制为一个对象。 通常,目标是管理全局应用程序状态。 我见过或写过的一些示例包括使用单例作为Web应用程序的配置设置的源,在客户端使用API密钥启动的任何操作(您通常不希望冒发送多个分析跟踪调用的风险,例如),并将数据存储在客户端Web应用程序的内存中(例如,存储在Flux中)。
单例应该由使用代码不可变,并且不应有实例化多个实例的危险。
注意:在某些情况下,单例可能很糟糕,而实际上它们总是很糟糕的论点。 对于该讨论,您可以查看有关此主题的这篇有用的文章 。
在JavaScript中创建单例的旧方法
用JavaScript编写单例的旧方法涉及利用闭包和立即调用的函数表达式 。 这是我们可能以旧方式为假设的Flux实现编写一个(非常简单的)存储的方法:
var UserStore = (function(){
var _data = [];
function add(item){
_data.push(item);
}
function get(id){
return _data.find((d) => {
return d.id === id;
});
}
return {
add: add,
get: get
};
}());
解释该代码后, UserStore
设置为立即调用的函数的结果—该对象具有两个功能,但不授予对数据集合的直接访问权限。
但是,此代码比所需的代码更加冗长,并且也无法满足我们使用单例时所希望的不变性。 稍后执行的代码可以修改公开的功能之一,甚至UserStore
完全重新定义UserStore
。 此外,修改/违规代码可能在任何地方! 如果由于UsersStore
的意外修改而导致bug,那么在较大的项目中进行跟踪可能会令人沮丧。
正如Ben Cherry在本文中所指出的,您可以采取一些更高级的措施来缓解其中的一些弊端。 (他的目标是创建模块,这些模块恰好是单例,但是模式是相同的。)但是那些模块给代码增加了不必要的复杂性,但仍然无法使我们得到我们想要的东西。
新方法
通过利用ES6功能(主要是模块)和新的const
变量声明,我们可以以更简洁,更能满足我们要求的方式编写单例。
让我们从最基本的实现开始。 这是上述示例的一种更简洁,更强大的现代解释:
const _data = [];
const UserStore = {
add: item => _data.push(item),
get: id => _data.find(d => d.id === id)
}
Object.freeze(UserStore);
export default UserStore;
如您所见,这种方式提高了可读性。 但是,真正UserStore
在于对代码的约束,该代码在这里消耗了我们的单例模块:消费代码由于const
关键字而无法重新分配UserStore
。 由于我们使用Object.freeze ,因此无法更改其方法,也无法向其添加新方法或属性。 此外,由于我们利用了ES6模块,因此我们确切知道UserStore
的使用位置。
现在,在这里我们使UserStore
成为对象文字。 大多数情况下,使用对象文字是最易读和简洁的选择。 但是,有时您可能想利用传统类的好处。 例如,Flux中的商店都将具有很多相同的基本功能。 利用传统的面向对象的继承是一种在保持代码DRY的同时获得重复功能的方法。
如果我们想利用ES6类,以下是实现的外观:
class UserStore {
constructor(){
this._data = [];
}
add(item){
this._data.push(item);
}
get(id){
return this._data.find(d => d.id === id);
}
}
const instance = new UserStore();
Object.freeze(instance);
export default instance;
这种方式比使用对象文字稍微冗长一些,并且我们的示例非常简单,以至于我们看不到使用类的任何好处(尽管在最终示例中会派上用场)。
类路线的好处可能并不明显,那就是,如果这是您的前端代码,而您的后端是用C#或Java编写的,则可以在客户端应用程序中采用许多相同的设计模式就像您在后端所做的那样,并提高了团队的效率(如果您很小,并且人们正在全力以赴)。 听起来很软且很难测量,但是我亲身体验了它在带有React前端的C#应用程序上的工作,并且好处是实实在在的。
应当注意,从技术上讲,使用这两种模式的单身人士的不变性和不可替代性可以被积极的挑衅者所破坏。 可以使用Object.assign复制对象文字,即使它本身是const
也可以。 而且,当我们导出类的实例时,尽管我们没有直接将类本身暴露给使用的代码,但是任何实例的构造函数都可以在JavaScript中使用,并且可以调用以创建新实例。 显然,尽管如此,所有这些都至少需要一点点努力,并且希望您的开发人员并不太坚持违反单例模式。
但是,让我们说,您要特别确保没有人弄乱您的单例的单一性,并且还希望它与面向对象世界中的单例的实现更加紧密地匹配。 您可以执行以下操作:
class UserStore {
constructor(){
if(! UserStore.instance){
this._data = [];
UserStore.instance = this;
}
return UserStore.instance;
}
//rest is the same code as preceding example
}
const instance = new UserStore();
Object.freeze(instance);
export default instance;
通过添加对实例的引用的额外步骤,我们可以检查是否已经实例化了UserStore
,如果已经实例化,则不会创建新实例。 如您所见,这还充分利用了我们使UserStore
成为类的事实。
有什么想法吗? 讨厌邮件?
毫无疑问,有很多开发人员已经在JavaScript中使用旧的singleton / module模式很多年了,他们发现它对他们来说效果很好。 但是,因为寻找更好的做事方式对于成为开发人员至关重要,因此希望我们看到这样一种更清洁,更易理解的模式越来越受欢迎。 尤其是一旦使用ES6 +功能变得更加容易和普遍的话。
这是我在生产中采用的一种模式,用于在自定义Flux实现中建立商店(商店比这里的示例运行得更多),并且运行良好。 但是,如果您看到其中的漏洞,请告诉我。 另外,请提倡您偏爱哪种新模式,是否认为对象字面量是首选,还是偏爱类!
From: https://www.sitepoint.com/javascript-design-patterns-singleton/