伴随着ECMA 国际组织提出的每一条新规范,在广泛使用的语言中,JavaScript是迄今为止发展最快的,看起来不像它最早的版本,更像Python。虽然这些变化有其公平的诋毁者,但新的JavaScript确实使代码更易于阅读和推理,更容易以遵循软件工程最佳实践的方式编写(特别是模块化和可靠的原则),更易于组装规范化软件设计模式。
解释ES6
ES6(亦称ES2015)是自从ES5在2009被标准化以来对语言的第一个主要更新。几乎所有的现代浏览器都支持ES6。然而,如果您需要适应旧浏览器,使用诸如Babel这样的工具,ES6代码可以很容易地被转译成ES5。ES6为JavaScript提供了大量新特性,包括类的高级语法和变量声明的新关键字。你可以通过阅读SITEPOLT文章来了解更多关于它的主题。
什么是单例
也许您还不熟悉单例模式,它的核心其实就是可以将类实例化为一个对象的一种设计模式。通常,目的是管理全局应用程序状态。我见过或写过的一些例子包括使用单例作为Web应用程序的配置的来源,在客户端用来启动应用的API密钥(例如,通常您不想冒着发送多个分析跟踪调用的风险),以及用来将数据存储在客户端Web应用程序的内存中(例如,Flux的stores)。
一个单例应该不能被消费代码修改,并且实例化多个单例不存在风险。
注意:单例可能有坏的情况,并且有些争论说它们总是很糟糕。对于这个讨论,你可以查看这个主题的有用文章(https://www.sitepoint.com/whats-so-bad-about-the-singleton/)。
JavaScript中创建单例的老方法
在JavaScript中编写单例的旧方法包括利用闭包并立即调用函数表达式。下面是我们如何编写一个(非常简单的)store来实现一个假设的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。而且,修改/违规代码可能在任何地方!如果由于UsersStore的意外修改而导致出现错误,在更大的项目中对其进行跟踪可能会非常令人沮丧。
正如Ben Cherry在本文中指定的那样,您可以采取更先进的措施来缓解这些缺点。 (他的目标是创建模块,这些模块恰好是单例模式,但模式是相同的。)但是这些模块增加了代码的不必要的复杂性,但仍然没有使我们确切地得到我们想要的。
新方法
通过利用ES6的特性,主要是模块和新的常量变量声明,我们不仅可以用更简洁的方式编写单例,而且更符合我们的要求。
我们从最基本的实现开始。这是对上述例子的一种更清晰和更强大的现代解释:
const _data = [];
const UserStore = {
add: item => _data.push(item),
get: id => _data.find(d => d.id === id)
}
Object.freeze(UserStore);
export default UserStore;
正如你所看到的,这种方式提高了可读性。但是它真正闪耀的地方在于强加在我们的小单例模块的代码上:由于const关键字,使用代码无法重新分配UserStore。由于我们使用Object.freeze,它的方法不能改变,新的方法或属性也不能添加到它。此外,因为我们正在利用ES6模块,所以我们知道UserStore的使用位置。
现在,我们在这里创建了一个对象文字。大多数情况下,使用对象字面值是最易读和简洁的选项。但是,有时您可能想利用传统类的好处。例如,Flux的stores都有很多相同的基本功能。利用传统的面向对象继承是在保持代码干爽的同时获得可复用功能的一种方法。
下面是我们想要使用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中使用旧的单例/模块模式已有多年,并且发现它对他们来说非常好用。然而,因为找到更好的方式去做事情对于成为开发者来说是至关重要的,希望我们能看到更清晰,更易于理解的模式,这种模式越来越受到人们的青睐。特别是一旦使用ES6的功能,它将变得更容易和更普遍。
这是我在生产中使用的一种模式,用于在定制的Flux实现中构建stores(stores比我们的示例稍多一些),并且运行良好。但是,如果你可以发现它的漏洞,请让我知道。另外请提倡你喜欢的新模式,以及你是否认为对象字面量是要走的路,或者如果你更喜欢类!
本文为翻译作品,原文来自sitepoint,作者Samier Saeed。原文地址:https://www.sitepoint.com/javascript-design-patterns-singleton/