跳到主要内容

单例模式

问题

什么是单例模式?如何在 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 连接复用同一连接
日志服务统一日志收集
缓存管理全局请求缓存
配置中心全局配置读取

相关链接