各位观众老爷,大家好!我是你们的老朋友,今天咱们聊聊JavaScript里的“单身贵族”——单例模式(Singleton Pattern)。
开场白:为啥要有单例?
想象一下,你是个皇帝,只有一个玉玺,盖章生效。如果突然冒出俩玉玺,那谁说了算?国家还不乱套了!单例模式就是保证,对于某些特别重要的类,我们只能有一个实例,确保全局只有一个入口,避免混乱。
单例模式是啥?
简单来说,单例模式就是限制一个类只能创建一个实例,并且提供一个全局访问点。这个访问点通常是一个静态方法,让你随时随地都能拿到这个唯一的实例。
为啥要用单例?
- 资源控制: 只有一个实例,意味着资源占用可控。比如,数据库连接,全局缓存,日志对象等等,共享一个实例可以节省资源。
- 数据一致性: 只有一个实例,所有操作都针对同一个对象,保证数据的一致性。
- 全局访问: 方便访问,不需要到处传递对象,直接通过单例类的静态方法就可以拿到实例。
- 配置管理: 全局配置对象,方便读取和修改配置信息。
单例模式的几种实现方式
接下来咱们来撸起袖子,写几个JavaScript版本的单例模式。
1. 饿汉式单例 (Eager Initialization)
这种方式最简单粗暴,类加载的时候就创建好实例,天生就是单例。
class Singleton {
static instance = new Singleton(); // 类加载时就创建实例
constructor() {
if (Singleton.instance) {
throw new Error("Singleton class cannot be instantiated directly. Use getInstance() method.");
}
// 初始化操作
this.data = "Hello from Singleton";
}
static getInstance() {
return Singleton.instance;
}
getData() {
return this.data;
}
}
// 使用
const instance1 = Singleton.getInstance();
const instance2 = Singleton.getInstance();
console.log(instance1 === instance2); // true,指向同一个实例
console.log(instance1.getData()); // Hello from Singleton
优点: 实现简单,线程安全(JavaScript是单线程)。
缺点: 无论是否使用,实例都会被创建,浪费资源。
2. 懒汉式单例 (Lazy Initialization)
这种方式比较懒,只有在第一次使用的时候才创建实例。
class Singleton {
static instance = null;
constructor() {
if (Singleton.instance) {
throw new Error("Singleton class cannot be instantiated directly. Use getInstance() method.");
}
// 初始化操作
this.data = "Hello from Singleton";
}
static getInstance() {
if (!Singleton.instance) {
Singleton.instance = new Singleton();
}
return Singleton.instance;
}
getData() {
return this.data;
}
}
// 使用
const instance1 = Singleton.getInstance();
const instance2 = Singleton.getInstance();
console.log(instance1 === instance2); // true,指向同一个实例
console.log(instance1.getData()); // Hello from Singleton
优点: 延迟加载,节省资源。
缺点: 在某些并发环境下可能存在线程安全问题(虽然在JavaScript中几乎可以忽略,但还是要注意)。
3. 使用闭包实现的单例
利用JavaScript的闭包特性,将实例保存在闭包中,防止被外部访问。
const Singleton = (function() {
let instance;
function createInstance() {
// 初始化操作
return {
data: "Hello from Singleton (Closure)",
getData: function() {
return this.data;
}
};
}
return {
getInstance: function() {
if (!instance) {
instance = createInstance();
}
return instance;
}
};
})();
// 使用
const instance1 = Singleton.getInstance();
const instance2 = Singleton.getInstance();
console.log(instance1 === instance2); // true,指向同一个实例
console.log(instance1.getData()); // Hello from Singleton (Closure)
优点: 隐藏了实现细节,避免外部直接修改实例。
缺点: 代码稍微复杂一些。
4. 使用ES6的Symbol实现单例
利用Symbol的唯一性,防止外部通过new
关键字创建实例。
const SINGLETON_KEY = Symbol('singleton');
class Singleton {
constructor(token) {
if (token !== SINGLETON_KEY) {
throw new Error('Cannot construct Singleton directly, use getInstance() method');
}
// 初始化操作
this.data = "Hello from Singleton (Symbol)";
}
static getInstance() {
if (!Singleton.instance) {
Singleton.instance = new Singleton(SINGLETON_KEY);
}
return Singleton.instance;
}
getData() {
return this.data;
}
}
Singleton.instance = null;
// 使用
const instance1 = Singleton.getInstance();
const instance2 = Singleton.getInstance();
console.log(instance1 === instance2); // true,指向同一个实例
console.log(instance1.getData()); // Hello from Singleton (Symbol)
// 尝试直接new Singleton()会报错
// const instance3 = new Singleton(); // Error: Cannot construct Singleton directly, use getInstance() method
优点: 更强的封装性,防止外部直接创建实例。
缺点: 代码稍微复杂一些。
5. TypeScript中的单例
TypeScript可以更好地利用private构造函数来保证单例。
class Singleton {
private static instance: Singleton | null = null;
private constructor() {
// 初始化操作
this.data = "Hello from Singleton (TypeScript)";
}
static getInstance(): Singleton {
if (!Singleton.instance) {
Singleton.instance = new Singleton();
}
return Singleton.instance;
}
getData(): string {
return this.data;
}
private data: string;
}
// 使用
const instance1 = Singleton.getInstance();
const instance2 = Singleton.getInstance();
console.log(instance1 === instance2); // true
console.log(instance1.getData()); // Hello from Singleton (TypeScript)
优点: 利用TypeScript的类型系统, 保证更好的代码质量和可维护性. private constructor
更加安全.
缺点: 需要使用TypeScript.
各种单例实现方式对比
实现方式 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
饿汉式 | 实现简单,线程安全(JS单线程) | 浪费资源,实例在类加载时就创建,无论是否使用 | 对资源占用不敏感,且实例必须存在的场景 |
懒汉式 | 延迟加载,节省资源 | 在某些并发环境下可能存在线程安全问题(JS几乎可以忽略) | 对资源占用敏感,且实例不是必须立即存在的场景 |
闭包 | 隐藏实现细节,避免外部直接修改实例 | 代码稍微复杂 | 需要隐藏实现细节,防止外部直接访问实例的场景 |
Symbol | 更强的封装性,防止外部直接创建实例 | 代码稍微复杂 | 需要更强封装性,防止外部通过new 关键字创建实例的场景 |
TypeScript | 利用TypeScript的类型系统, 保证更好的代码质量和可维护性, private constructor 更加安全. |
需要使用TypeScript. | 需要使用TypeScript, 并且希望利用类型系统提供更安全单例实现的场景. |
单例模式的应用场景
- 配置管理: 将配置信息保存在单例对象中,方便全局访问。
- 数据库连接池: 维护一个数据库连接池单例,避免频繁创建和销毁连接。
- 日志对象: 使用单例的日志对象,统一记录日志信息。
- 任务队列: 单例的任务队列,保证任务按顺序执行。
- 全局缓存: 使用单例的缓存对象,存储全局数据。
- 用户会话管理: 在服务端,可以使用单例来管理用户会话信息。
单例模式的注意事项
- 过度使用: 不要滥用单例模式,只在真正需要全局唯一实例的场景下使用。
- 测试: 单例模式可能会增加单元测试的难度,需要注意测试策略。
- 状态管理: 单例对象的状态是全局共享的,需要注意状态的管理,避免出现意外的副作用。
- 模块化: 在模块化开发中,可以使用模块来代替单例模式,提供更好的封装性和可测试性。
一些“不正经”的单例使用场景
- 后宫佳丽三千,皇上只有一个! (虽然可能有很多皇子,但皇上只有一个)
- 一个网站的管理员账号,只有一个! (当然,可以有多个权限不同的管理员,但最高权限的那个只有一个)
- 一个班的班长,只有一个! (副班长不算!)
总结
单例模式是一种常用的设计模式,可以保证一个类只有一个实例,并提供全局访问点。在JavaScript中,有多种方式可以实现单例模式,选择哪种方式取决于具体的应用场景和需求。希望通过今天的讲解,大家能够对单例模式有更深入的理解,并在实际开发中灵活运用。
记住,单例虽好,可不要贪杯哦!合理使用才能发挥它的最大价值。
最后的彩蛋
其实,在JavaScript中,很多内置对象都是单例的,比如window
对象,document
对象等等。它们都是全局唯一的,可以直接访问。
好了,今天的讲座就到这里,感谢大家的收看!下次再见!