本文主要介绍单例模式,以及随着 JavaScript 的发展,我们如何在 JavaScript 中实现单例模式。
设计模式(Design Patterns)
设计模式是在软件设计中解决问题的特定方法,由于这些问题出现频繁,慢慢就形成了解决这类问题的一种特定模式。设计模式是软件开发的基本部分,它不是特定的软件,而是解决重复出现问题的一种方法。
随着软件架构的改变,生态系统的发展,新的问题也催生出新的设计模式。就 Web 生态而言,不同的框架也会催生出新的模式,或者优化、修改旧的模式。理解这些模式有助于我们明析软件开发中的概念,可以帮助和优化改进应用程序。
单例模式(Singleton Pattern)
单例模式可以限制类只能实例化一次,这在有全局共享资源时非常有用,例如:应用程序的配置,React 程序的全局状态。单例应该是不会被使用者修改的,并且多次实例化也不会有危险。
JavaScript 中的单例
随着语言的发展,尤其 ES6(ES2015)为 JavaScript 提供了大量的新特性(这也是为什么 ES6 最常被提及),其中包括新的
class
语法、新的声明变量的关键字(const
、let
)等等,这也使得实现有些不同。ES5
旧的实现方法依赖于闭包(closures)和立即执行函数(IIFE),下面是一个应用程序的全局状态:
var AppStore = (function() { var _todos = []; function add(item) { _todos.push(item); } function get(id) { return _todos.find((d) => { return d.id === id; }); } return { add: add, get: get } }());
当程序执行时,匿名函数会立即调用并返回一个带有两个方法的对象,但是没有真实存储数据集合
_todos
的访问权限(闭包的作用)。但是,返回的对象的方法仍然可以被修改,甚至可以完全重新定义
AppStore
,而且这些修改可能出现在任何地方,这让在大项目中追踪问题非常困难。我们希望阻止这种修改,ES6 的新特性将派上用场。ES6
现在,全新的
const
关键字和模块系统将帮助我们解决之前的问题,并且代码更简洁。const _todos = []; const AppStore = { add: item => _todos.push(item), get: id => _todos.find(d => d.id === id) } Object.freeze(AppStore); export default AppStore;
但这和上面提到的
class
又有什么关系?让我们用类改写:class AppStore { constructor(){ this._todos = []; } add(item){ this._todos.push(item); } get(id){ return this._todos.find(d => d.id === id); } } const instance = new AppStore(); Object.freeze(instance); export default instance;
如果你是一个有经验的 JavaScript 使用者,你会发现两种实现方式的单例都不能保证其不变性和不可重写性。
- 第一种方式导出的对象虽然是无法被重新赋值,属性也被冻结,但是仍然可以使用 Object.assign 对其进行复制。
- 第二种方式虽然没有暴露出类供使用者调用,但是导出的实例的构造器仍然是可用的,可以用来创建新的实例。
此时,
class
的优势出现了,我们可以对其构造器进行改进:class AppStore { constructor(){ // 还没有实例,对其实例化 if(!AppStore.instance) { this._todos = []; AppStore.instance = this; } // 实例化过,直接返回 return AppStore.instance; } add(item){ this._todos.push(item); } get(id){ return this._todos.find(d => d.id === id); } } const instance = new AppStore(); Object.freeze(instance); export default instance;
现在,额外保存实例并在构造器中进行判断,实例化过返回之前保存的实例,以此来确保应用中有且仅有一个实例。