前端开发设计模式: 单例模式(Singleton Pattern)
什么是单例模式?(Singleton Pattern)
单例模式,也叫单体模式,是一种创建型设计模式,是全局(或某一作用域范围)唯一实例,大家共享、复用一个实例对象。—— 最基础、最常见的设计模式
1、保证对象实例只创建一次,后续的引用都是同一个实例对象
2、保证一个类只有一个实例,并提供一个访问它的 全局访问点
单例模式特点:
- 唯一性:只有一个实例,无论在程序的任何地方访问这个类,都将得到同一个实例
- 全局访问:提供了一种全局访问该实例的方式,使得在整个程序中都可以方便地使用这个唯一的实例
为什么要用单例模式?
单例模式的优点
1、减少资源消耗:
对于一些需要频繁创建但又只需一个实例的对象,如全局状态管理、日志记录器等,使用单例模式可以避免重复创建对象带来的资源浪费和性能问题
2、全局访问
提供了一个简单的方式来访问唯一的实例,方便在不同的模块中使用
3、易于管理
由于只有一个实例,对于一些需要统一管理的对象,如配置对象、全局缓存等,使用单例模式可以方便地进行管理和维护。
单例模式的缺点
1、测试困难
由于单例通常是全局可访问的,这使得在单元测试中难以模拟和控制其行为,可能会导致测试的复杂性增加
2、违法单一职责原则
单例对象可能承担过多的职责,不利于代码的可维护性和扩展性
3、可能导致内存泄漏
如果单例对象在整个应用的生命周期中都存在,并且持有一些资源(如数据库连接、文件句柄等),如果不及时释放这些资源,可能会导致内存泄漏
单例模式的应用场景
1、全局状态管理
在前端应用中,可能需要一个全局的状态管理器来存储和管理应用的状态。使用单例模式可以确保只有一个状态管理器实例,避免状态的混乱和不一致。
2、日志记录器
日志记录器通常需要在整个应用中共享,以便在不同的地方记录日志。使用单例模式可以确保只有一个日志记录器实例,方便进行日志的统一管理和输出。
例如,可以创建一个单例的日志记录器对象,提供方法来记录不同级别的日志信息,并将日志输出到控制台或文件中。
3、数据库连接
在与数据库进行交互时,通常需要建立数据库连接。使用单例模式可以确保只有一个数据库连接实例,避免重复建立连接带来的资源浪费和性能问题。
例如,可以创建一个单例的数据库连接对象,提供方法来执行数据库查询和更新操作,并在应用启动时建立连接,在应用关闭时关闭连接。
常见的场景就是:web 实时通信 SSE连接
如何实现单例模式?
单例模式实现有多种方式,如下:
一、全局对象(不推荐)
两种实现方式:
- 在全局环境中用 var 字面量声明一个对象,利用 var 的变量提升 + 全局属性的特点(全局环境下的 var 变量会自动成为全局属性),所以慎用 var
- 直接挂载到 全局对象 window 上
优点:
使用简单
缺点:
会存在全局污染
实例如下:
// 例1: window.jQuery = window.$ = jQuery; // 例2: window._ = lodash // 例3: // 全局环境下的 var 变量会自动成为全局属性 var singleUser = { name: 'sam', id: 1001 } //使用 console.log(singleUser.name) // sam console.log(window.singleUser.name) // sam
二、立即执行函数 + 闭包实现
思路:利用 JS 的闭包 来保存那个唯一对象实例,这样就可以 通过 new 来获取唯一实例对象。
实例如下:
const GlobalUser = (function () { let instance // 闭包保存的唯一实例对象 function createInstance(obj){ return obj } return function(obj) { if(!instance){ instance = createInstance(obj) } return instance } })() // 立即执行,外层函数的价值就是他的闭包变量 instance console.log(new GlobalUser({name: 'sam', id: '1001'}).name) // sam // 依然是 sam,复用了第一次创建的实例 console.log(new GlobalUser({name: 'baby', id: '1002'}).name) // sam console.log(new GlobalUser() === new GlobalUser()) // true
三、ES6 实现
1、ES6 类
class Singleton { constructor(){ if(!Singleton.instance){ Singleton.instance = this } return Singleton.instance; } }
2、ES6 模块 Module
ES6 的模块其实就是单例模式,模块中导出的对象就是单例的,多次导入其实是同一个引用。
- Singleton 模式:import 模块的代码只会执行一次,同一个 url 文件只会第一次导入时执行代码。后续任何地方 import 都不会执行模块代码了,也就是说,import 语句是 Singleton 模式的。
- 只读 - 共享:模块导入的接口的是只读的,不能修改。当然引用对象的属性值是可以修改的,不建议这么干,注意模块是共享的,导出的是一个引用,修改后其他方也会生效。
因此用 ESM 实现单例就比较简单了。
// 模块声明 config.js export default { title: '设计模式' }
// 使用 import config from './config.js' console.log(config) // {title: '设计模式'} config.title = '修改一下' import config2 from './config.js' console.log(config, config2) // {title: '修改一下'} {title: '修改一下'}
单例模式的注意事项
1、线程安全
在多线程环境下,需要确保单例的创建是线程安全的。可以使用锁或其他同步机制来保证在多个线程同时访问单例时,只有一个实例被创建
2、延迟加载
可以考虑使用延迟加载的方式来创建单例实例,即在第一次访问单例时才创建实例。这样可以避免在应用启动时就创建一些可能不需要的对象,提高应用的启动速度。
3、可扩展性
在设计单例类时,要考虑到未来可能的扩展需求。尽量保持单例类的接口简洁和可扩展,以便在需要时可以方便地添加新的功能。