什么是单例模式?
单例模式是一种创建型设计模式,它确保一个类只有一个实例,并提供一个访问该实例的全局访问点。
想象一下,你正在开发一个系统,需要一个全局的配置管理器。无论在系统的哪个地方访问这个配置管理器,你都希望它是同一个实例,这就是单例模式的典型应用场景。
单例模式的实现方式
1. 基础实现
class ConfigManager {
static instance = null;
constructor() {
if (ConfigManager.instance) {
return ConfigManager.instance;
}
this.config = {};
ConfigManager.instance = this;
}
setConfig(key, value) {
this.config[key] = value;
}
getConfig(key) {
return this.config[key];
}
}
// 使用示例
const config1 = new ConfigManager();
const config2 = new ConfigManager();
console.log(config1 === config2); // true,证明是同一个实例
2. 闭包实现(更安全的方式)
const ConfigManager = (function() {
let instance = null;
class Config {
constructor() {
this.config = {};
}
setConfig(key, value) {
this.config[key] = value;
}
getConfig(key) {
return this.config[key];
}
}
return {
getInstance: function() {
if (!instance) {
instance = new Config();
}
return instance;
}
};
})();
// 使用示例
const config1 = ConfigManager.getInstance();
const config2 = ConfigManager.getInstance();
console.log(config1 === config2); // true
实战示例
单例模式通常适用于以下场景:
- 全局状态管理:配置管理器、数据存储、主题管理
- 资源共享:数据库连接池、线程池、缓存
- 系统组件:模态框、全局通知、登录框
现在我来举例演示一下面试中常被问到的两种实践方案
本地存储管理器(Storage)
静态方法版
// 定义Storage
class Storage {
static getInstance() {
// 判断是否已经new过1个实例
if (!Storage.instance) {
// 若这个唯一的实例不存在,那么先创建它
Storage.instance = new Storage()
}
// 如果这个唯一的实例已经存在,则直接返回
return Storage.instance
}
getItem (key) {
return localStorage.getItem(key)
}
setItem (key, value) {
return localStorage.setItem(key, value)
}
}
const storage1 = Storage.getInstance()
const storage2 = Storage.getInstance()
storage1.setItem('name', '李雷')
// 李雷
storage1.getItem('name')
// 也是李雷
storage2.getItem('name')
// 返回true
storage1 === storage2
闭包版
// 先实现一个基础的StorageBase类,把getItem和setItem方法放在它的原型链上
function StorageBase () {}
StorageBase.prototype.getItem = function (key){
return localStorage.getItem(key)
}
StorageBase.prototype.setItem = function (key, value) {
return localStorage.setItem(key, value)
}
// 以闭包的形式创建一个引用自由变量的构造函数
const Storage = (function(){
let instance = null
return function(){
// 判断自由变量是否为null
if(!instance) {
// 如果为null则new出唯一实例
instance = new StorageBase()
}
return instance
}
})()
// 这里其实不用 new Storage 的形式调用,直接 Storage() 也会有一样的效果
const storage1 = new Storage()
const storage2 = new Storage()
storage1.setItem('name', '李雷')
// 李雷
storage1.getItem('name')
// 也是李雷
storage2.getItem('name')
// 返回true
storage1 === storage2
全局模态框(Modal)
这道题比较经典,基本上所有讲单例模式的文章都会以此为例,同时它也是早期单例模式在前端领域的最集中体现
静态方法版
class Modal {
static instance = null;
constructor() {
if (Modal.instance) {
return Modal.instance;
}
this.modalElement = null;
this.createModal();
Modal.instance = this;
}
createModal() {
this.modalElement = document.createElement('div');
this.modalElement.style.display = 'none';
document.body.appendChild(this.modalElement);
}
show(content) {
this.modalElement.innerHTML = content;
this.modalElement.style.display = 'block';
}
hide() {
this.modalElement.style.display = 'none';
}
}
// 使用示例
const modal1 = new Modal();
const modal2 = new Modal();
modal1.show('Hello World!');
console.log(modal1 === modal2); // true
闭包版
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>单例模式弹框</title>
</head>
<style>
#modal {
height: 200px;
width: 200px;
line-height: 200px;
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
border: 1px solid black;
text-align: center;
}
</style>
<body>
<button id='open'>打开弹框</button>
<button id='close'>关闭弹框</button>
</body>
<script>
// 核心逻辑,这里采用了闭包思路来实现单例模式
const Modal = (function() {
let modal = null
return function() {
if(!modal) {
modal = document.createElement('div')
modal.innerHTML = '我是一个全局唯一的Modal'
modal.id = 'modal'
modal.style.display = 'none'
document.body.appendChild(modal)
}
return modal
}
})()
// 点击打开按钮展示模态框
document.getElementById('open').addEventListener('click', function() {
// 未点击则不创建modal实例,避免不必要的内存占用;此处不用 new Modal 的形式调用也可以,和 Storage 同理
const modal = new Modal()
modal.style.display = 'block'
})
// 点击关闭按钮隐藏模态框
document.getElementById('close').addEventListener('click', function() {
const modal = new Modal()
if(modal) {
modal.style.display = 'none'
}
})
</script>
</html>
为什么说闭包实现单例模式更安全?【拓展】
1. 构造函数方式可能被篡改:
构造函数方式:
const c1 = new ConfigManager();
ConfigManager.instance = null; // 破坏单例状态
const c2 = new ConfigManager(); // 会创建新实例
console.log(c1 === c2); // false
这种方式的问题:
- instance 属性虽然是静态的,但仍可以被外部访问和修改
- 可以通过 Singleton.instance = null 破坏单例
闭包方式无法篡改:
const c1 = ConfigManager.getInstance();
// 无法直接访问或修改 instance 变量
const c2 = ConfigManager.getInstance();
console.log(s1 === s2); // true
优势:
- instance 变量完全私有,外部无法访问
- 只能通过 getInstance 方法获取实例
- 实例的创建过程被完全封装
2. 初始化控制更严格
闭包比构造函数有更灵活的初始化时机,让我用一个具体的例子来解释这句话
假设我们有一个数据库连接的单例:
// 构造函数方式
class DatabaseConnection {
static instance = null;
constructor() {
if (DatabaseConnection.instance) {
return DatabaseConnection.instance;
}
this.connect(); // 连接数据库
DatabaseConnection.instance = this;
}
connect() {
// 建立数据库连接
console.log('建立数据库连接...');
}
}
// 在程序启动时就创建实例
const db = new DatabaseConnection();
这种方式的问题是:即使我们还没有用到数据库,程序一启动就会创建连接,浪费了系统资源。
而使用闭包的方式:
// 闭包方式
const DatabaseConnection = (function() {
let instance = null;
function init() {
// 只有在真正需要时才建立连接
return {
connect() {
console.log('建立数据库连接...');
}
};
}
return {
getInstance() {
if (!instance) {
instance = init(); // 延迟到第一次调用时才初始化
}
return instance;
}
};
})();
// 只有在真正需要时才获取实例
const db = DatabaseConnection.getInstance();
这种方式的优势:
- 实例只在第一次调用 getInstance 时才创建
- 避免了资源的浪费,这就是所谓的"懒加载"(Lazy Loading)策略,它确保了实例只在真正需要时才被创建,而不是在程序一启动就创建。这在处理资源密集型的对象(如数据库连接、文件系统等)时特别有用。
单例模式的优缺点
优点:
- 保证一个类只有一个实例
- 提供了对该实例的全局访问点
- 节约系统资源
- 避免对共享资源的多重占用
缺点:
- 违反单一职责原则
- 可能掩盖不良设计
- 在并发环境下需要特别注意线程安全
- 测试可能会变得困难
总结
单例模式是一种简单但强大的设计模式,它在需要确保全局唯一性的场景中非常有用。但要注意,过度使用单例可能导致代码耦合度过高,因此在使用时需要权衡利弊,选择合适的场景。