跳到主要内容

JavaScript 单例模式

什么是单例模式?

单例模式是一种创建型设计模式,它确保一个类只有一个实例,并提供一个全局访问点来获取该实例。在JavaScript中,单例模式特别有用,因为它可以帮助我们管理应用程序的状态、协调系统操作并控制资源访问。

核心思想

无论创建多少次,单例模式总是返回同一个实例。

为什么需要单例模式?

在开发中,有些对象我们只需要一个实例:

  • 管理应用程序的配置信息
  • 管理共享资源(如数据库连接)
  • 控制对某些资源的并发访问
  • 协调系统中的行为

如果创建多个实例,可能会导致不必要的内存占用或产生逻辑错误。

单例模式的基本实现

下面是JavaScript中实现单例模式的最简方法:

javascript
const Singleton = (function() {
let instance;

function createInstance() {
// 这里可以是任何对象或值
return {
name: "我是单例对象",
printName: function() {
console.log(this.name);
}
};
}

return {
getInstance: function() {
if (!instance) {
instance = createInstance();
}
return instance;
}
};
})();

// 使用单例
const instance1 = Singleton.getInstance();
const instance2 = Singleton.getInstance();

console.log(instance1 === instance2); // 输出: true
instance1.printName(); // 输出: 我是单例对象

在这个实现中:

  1. 我们使用了立即执行函数(IIFE)创建一个闭包
  2. instance变量保存在闭包中,外部无法直接访问
  3. getInstance()方法检查实例是否已经创建,如果没有才创建新实例

ES6类实现单例模式

使用ES6的类语法,我们可以这样实现单例模式:

javascript
class Singleton {
constructor() {
if (Singleton.instance) {
return Singleton.instance;
}

// 实例属性初始化
this.name = "ES6单例";
this.data = [];

// 保存实例
Singleton.instance = this;
}

// 实例方法
addData(item) {
this.data.push(item);
}

getData() {
return this.data;
}
}

// 使用
const s1 = new Singleton();
const s2 = new Singleton();

console.log(s1 === s2); // 输出: true

s1.addData('测试数据');
console.log(s2.getData()); // 输出: ['测试数据']

单例模式的延迟初始化

有时我们不希望在程序启动时就创建单例对象,而是在第一次需要时才创建。这称为"延迟初始化"(Lazy Initialization):

javascript
class LazySingleton {
constructor() {
// 构造函数私有
}

static getInstance() {
if (!this.instance) {
this.instance = new LazySingleton();
}
return this.instance;
}

showMessage() {
console.log("我是延迟初始化的单例");
}
}

// 使用
const instance1 = LazySingleton.getInstance();
instance1.showMessage(); // 输出: 我是延迟初始化的单例

实际应用场景

1. 配置管理

单例模式非常适合管理应用程序的配置信息:

javascript
class ConfigManager {
constructor() {
if (ConfigManager.instance) {
return ConfigManager.instance;
}

this.config = {
apiUrl: 'https://api.example.com',
timeout: 3000,
debug: false
};

ConfigManager.instance = this;
}

get(key) {
return this.config[key];
}

set(key, value) {
this.config[key] = value;
}
}

// 使用
const config = new ConfigManager();
console.log(config.get('apiUrl')); // 输出: https://api.example.com

2. 模态框管理

在网页应用中,通常只需要一个模态框组件,不同内容可以动态加载:

javascript
class ModalManager {
constructor() {
if (ModalManager.instance) {
return ModalManager.instance;
}

this.modal = null;
this.createModalElement();
ModalManager.instance = this;
}

createModalElement() {
// 创建模态框DOM元素
this.modal = document.createElement('div');
this.modal.className = 'modal';
this.modal.innerHTML = `
<div class="modal-content">
<span class="close">&times;</span>
<div class="modal-body"></div>
</div>
`;
document.body.appendChild(this.modal);

// 添加关闭按钮事件
this.modal.querySelector('.close').addEventListener('click', () => {
this.hide();
});
}

show(content) {
this.modal.querySelector('.modal-body').innerHTML = content;
this.modal.style.display = 'block';
}

hide() {
this.modal.style.display = 'none';
}
}

// 使用
const modalManager = new ModalManager();
document.getElementById('showModal').addEventListener('click', () => {
modalManager.show('<p>这是模态框内容</p>');
});

3. 数据存储

管理应用程序中的状态或共享数据:

javascript
class Store {
constructor() {
if (Store.instance) {
return Store.instance;
}

this._data = {};
Store.instance = this;
}

set(key, value) {
this._data[key] = value;
}

get(key) {
return this._data[key];
}

clear() {
this._data = {};
}
}

// 使用
const store = new Store();
store.set('currentUser', { id: 1, name: 'Alice' });

// 在应用程序的其他地方
const anotherStore = new Store();
console.log(anotherStore.get('currentUser')); // 输出: { id: 1, name: 'Alice' }

单例模式的优缺点

优点

  1. 节约资源:只创建一个实例,减少内存占用
  2. 方便协调行为:提供全局访问点,便于协调不同部分的交互
  3. 保证一致性:确保状态在应用程序中保持一致

缺点

  1. 全局状态:引入了全局状态,可能导致代码耦合
  2. 测试困难:全局状态使单元测试变得复杂
  3. 隐藏依赖:依赖单例的代码可能隐藏依赖关系
谨慎使用

不要过度使用单例模式。对于真正需要全应用共享的功能,单例是合适的;但对大多数情况,依赖注入可能是更好的选择。

单例模式与模块模式

在JavaScript中,模块模式(Module Pattern)和单例模式有很多相似之处,因为它们都利用闭包创建私有状态。事实上,使用模块化系统(如ES6模块)时,导出对象本身就具有单例特性:

javascript
// singleton.js
const instance = {
data: [],
add(item) {
this.data.push(item);
},
get() {
return this.data;
}
};

export default instance;

由于ES模块是单例的,无论在多少个地方导入这个模块,都会得到同一个实例:

javascript
// file1.js
import singleton from './singleton.js';
singleton.add('来自file1');

// file2.js
import singleton from './singleton.js';
console.log(singleton.get()); // 输出: ['来自file1']

总结

单例模式是一种简单而强大的设计模式,适用于需要全局协调的场景。在JavaScript中有多种方式实现单例,从传统的构造函数和原型模式到现代的ES6类和模块系统。虽然单例模式非常有用,但也应谨慎使用,避免创建过多的全局状态。

练习

  1. 创建一个简单的主题管理器(ThemeManager)单例,允许切换应用的主题(light/dark)
  2. 实现一个记录用户操作的日志管理器(LogManager)单例
  3. 使用单例模式创建一个购物车(ShoppingCart),确保整个应用只有一个购物车实例

额外资源

单例模式是最简单但也是最容易误用的设计模式之一。掌握它的精髓,将帮助你在合适的场景下构建更优雅的JavaScript应用程序。