单例模式
问题
什么是单例模式?如何在 JavaScript/TypeScript 中实现单例模式?有哪些实际应用场景?
答案
单例模式(Singleton Pattern)确保一个类只有一个实例,并提供一个全局访问点。它是最简单也是最常用的设计模式之一。
核心概念
特点
| 特点 | 说明 |
|---|---|
| 唯一性 | 全局只有一个实例 |
| 全局访问 | 提供统一访问点 |
| 延迟初始化 | 可选,首次使用时创建 |
| 线程安全 | 多线程环境需考虑 |
实现方式
1. 简单对象字面量
// 最简单的单例 - 对象字面量
const singleton = {
name: 'singleton',
method() {
return 'Hello';
},
};
export default singleton;
2. 闭包实现(懒汉式)
// 懒汉式:首次调用时创建实例
const Singleton = (function () {
let instance: SingletonClass | null = null;
class SingletonClass {
private data: string;
constructor() {
this.data = 'Singleton Data';
}
getData() {
return this.data;
}
setData(data: string) {
this.data = data;
}
}
return {
getInstance(): SingletonClass {
if (!instance) {
instance = new SingletonClass();
}
return instance;
},
};
})();
// 使用
const s1 = Singleton.getInstance();
const s2 = Singleton.getInstance();
console.log(s1 === s2); // true
3. ES6 类实现
class Singleton {
private static instance: Singleton | null = null;
private data: Map<string, unknown> = new Map();
// 私有构造函数
private constructor() {}
static getInstance(): Singleton {
if (!Singleton.instance) {
Singleton.instance = new Singleton();
}
return Singleton.instance;
}
set(key: string, value: unknown) {
this.data.set(key, value);
}
get(key: string) {
return this.data.get(key);
}
}
// 使用
const instance1 = Singleton.getInstance();
const instance2 = Singleton.getInstance();
console.log(instance1 === instance2); // true
instance1.set('name', 'test');
console.log(instance2.get('name')); // 'test'
4. ES Module 天然单例
// store.ts
class Store {
private state: Record<string, unknown> = {};
getState() {
return this.state;
}
setState(newState: Record<string, unknown>) {
this.state = { ...this.state, ...newState };
}
}
// ES Module 导出的实例天然是单例
export const store = new Store();
// 在其他文件中
// import { store } from './store';
// 所有导入都指向同一个实例
5. 代理实现
class Database {
connect() {
console.log('Connected to database');
}
query(sql: string) {
console.log('Executing:', sql);
}
}
// 使用 Proxy 实现单例
const createSingleton = <T extends object>(
ClassName: new () => T
): new () => T => {
let instance: T | null = null;
return new Proxy(ClassName, {
construct(target) {
if (!instance) {
instance = new target();
}
return instance;
},
});
};
const SingletonDatabase = createSingleton(Database);
const db1 = new SingletonDatabase();
const db2 = new SingletonDatabase();
console.log(db1 === db2); // true
实际应用场景
1. 全局弹窗管理
class Modal {
private static instance: Modal | null = null;
private container: HTMLDivElement | null = null;
private constructor() {}
static getInstance(): Modal {
if (!Modal.instance) {
Modal.instance = new Modal();
}
return Modal.instance;
}
show(content: string) {
if (!this.container) {
this.container = document.createElement('div');
this.container.className = 'modal-container';
document.body.appendChild(this.container);
}
this.container.innerHTML = `
<div class="modal-overlay">
<div class="modal-content">
${content}
<button onclick="Modal.getInstance().hide()">关闭</button>
</div>
</div>
`;
this.container.style.display = 'block';
}
hide() {
if (this.container) {
this.container.style.display = 'none';
}
}
}
// 使用
Modal.getInstance().show('提示信息');
2. 全局状态管理
interface State {
user: { name: string; id: string } | null;
theme: 'light' | 'dark';
locale: string;
}
class GlobalStore {
private static instance: GlobalStore;
private state: State = {
user: null,
theme: 'light',
locale: 'zh-CN',
};
private listeners: Set<(state: State) => void> = new Set();
private constructor() {}
static getInstance(): GlobalStore {
if (!GlobalStore.instance) {
GlobalStore.instance = new GlobalStore();
}
return GlobalStore.instance;
}
getState(): State {
return this.state;
}
setState(partial: Partial<State>) {
this.state = { ...this.state, ...partial };
this.notify();
}
subscribe(listener: (state: State) => void) {
this.listeners.add(listener);
return () => this.listeners.delete(listener);
}
private notify() {
this.listeners.forEach((listener) => listener(this.state));
}
}
// 使用
const store = GlobalStore.getInstance();
store.subscribe((state) => {
console.log('State changed:', state);
});
store.setState({ user: { name: 'John', id: '1' } });
3. 日志管理器
type LogLevel = 'debug' | 'info' | 'warn' | 'error';
class Logger {
private static instance: Logger;
private level: LogLevel = 'info';
private logs: Array<{ level: LogLevel; message: string; timestamp: Date }> = [];
private constructor() {}
static getInstance(): Logger {
if (!Logger.instance) {
Logger.instance = new Logger();
}
return Logger.instance;
}
setLevel(level: LogLevel) {
this.level = level;
}
private log(level: LogLevel, message: string) {
const levels: LogLevel[] = ['debug', 'info', 'warn', 'error'];
if (levels.indexOf(level) >= levels.indexOf(this.level)) {
const entry = { level, message, timestamp: new Date() };
this.logs.push(entry);
console.log(`[${level.toUpperCase()}] ${message}`);
}
}
debug(message: string) {
this.log('debug', message);
}
info(message: string) {
this.log('info', message);
}
warn(message: string) {
this.log('warn', message);
}
error(message: string) {
this.log('error', message);
}
getLogs() {
return [...this.logs];
}
}
// 使用
const logger = Logger.getInstance();
logger.setLevel('debug');
logger.info('Application started');
logger.error('Something went wrong');
4. 请求缓存
class RequestCache {
private static instance: RequestCache;
private cache: Map<string, { data: unknown; expiry: number }> = new Map();
private defaultTTL = 5 * 60 * 1000; // 5分钟
private constructor() {}
static getInstance(): RequestCache {
if (!RequestCache.instance) {
RequestCache.instance = new RequestCache();
}
return RequestCache.instance;
}
async fetch<T>(url: string, options?: RequestInit): Promise<T> {
const cacheKey = `${url}_${JSON.stringify(options)}`;
const cached = this.cache.get(cacheKey);
if (cached && cached.expiry > Date.now()) {
return cached.data as T;
}
const response = await fetch(url, options);
const data = await response.json();
this.cache.set(cacheKey, {
data,
expiry: Date.now() + this.defaultTTL,
});
return data as T;
}
clear() {
this.cache.clear();
}
}
// 使用
const cache = RequestCache.getInstance();
const data = await cache.fetch('/api/users');
React 中的单例
使用 Context 实现
import { createContext, useContext, useState, ReactNode } from 'react';
interface AuthContextType {
user: { name: string } | null;
login: (name: string) => void;
logout: () => void;
}
const AuthContext = createContext<AuthContextType | null>(null);
export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<{ name: string } | null>(null);
const login = (name: string) => setUser({ name });
const logout = () => setUser(null);
return (
<AuthContext.Provider value={{ user, login, logout }}>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within AuthProvider');
}
return context;
}
使用 Zustand(推荐)
import { create } from 'zustand';
interface BearStore {
bears: number;
increase: () => void;
reset: () => void;
}
// Zustand store 天然是单例
export const useBearStore = create<BearStore>((set) => ({
bears: 0,
increase: () => set((state) => ({ bears: state.bears + 1 })),
reset: () => set({ bears: 0 }),
}));
// 任何组件使用都是同一个 store
function BearCounter() {
const bears = useBearStore((state) => state.bears);
return <h1>{bears} bears</h1>;
}
常见面试问题
Q1: 单例模式的优缺点?
答案:
| 优点 | 缺点 |
|---|---|
| 内存只有一个实例,节省资源 | 违反单一职责原则 |
| 全局访问点,方便管理 | 隐藏依赖关系,难以测试 |
| 避免频繁创建销毁 | 扩展困难 |
| 状态全局共享 | 可能造成全局状态污染 |
Q2: JavaScript 中如何保证单例?
答案:
// 方法 1:ES Module 导出实例
export const instance = new MyClass();
// 方法 2:静态属性 + 私有构造函数
class Singleton {
private static instance: Singleton;
private constructor() {}
static getInstance() {
if (!Singleton.instance) {
Singleton.instance = new Singleton();
}
return Singleton.instance;
}
}
// 方法 3:闭包
const Singleton = (() => {
let instance: MyClass;
return {
getInstance: () => instance || (instance = new MyClass()),
};
})();
Q3: 懒汉式和饿汉式的区别?
答案:
| 类型 | 创建时机 | 优点 | 缺点 |
|---|---|---|---|
| 懒汉式 | 首次使用时创建 | 延迟加载,节省资源 | 需要判断逻辑 |
| 饿汉式 | 类加载时创建 | 实现简单,线程安全 | 可能浪费资源 |
// 懒汉式
class LazySingleton {
private static instance: LazySingleton;
static getInstance() {
if (!LazySingleton.instance) {
LazySingleton.instance = new LazySingleton();
}
return LazySingleton.instance;
}
}
// 饿汉式(ES Module 导出)
export const instance = new MyClass(); // 模块加载时创建
Q4: 单例模式如何进行单元测试?
答案:
// 问题:单例状态会在测试间累积
// 解决:提供重置方法
class TestableSingleton {
private static instance: TestableSingleton | null = null;
private data = 0;
private constructor() {}
static getInstance() {
if (!TestableSingleton.instance) {
TestableSingleton.instance = new TestableSingleton();
}
return TestableSingleton.instance;
}
// 测试用:重置实例
static resetInstance() {
TestableSingleton.instance = null;
}
increment() {
this.data++;
}
getData() {
return this.data;
}
}
// 测试
describe('TestableSingleton', () => {
beforeEach(() => {
TestableSingleton.resetInstance();
});
it('should start with 0', () => {
expect(TestableSingleton.getInstance().getData()).toBe(0);
});
});
Q5: 前端哪些场景适合使用单例?
答案:
| 场景 | 说明 |
|---|---|
| 全局状态管理 | Redux store、Vuex store |
| 弹窗管理器 | 确保只有一个弹窗层 |
| WebSocket 连接 | 复用同一连接 |
| 日志服务 | 统一日志收集 |
| 缓存管理 | 全局请求缓存 |
| 配置中心 | 全局配置读取 |