88. Singleton (Одиночка)
Singleton — паттерн проектирования, гарантирующий что класс имеет только один экземпляр и предоставляющий глобальную точку доступа к нему. В JavaScript реализуется несколькими способами — от простого объекта до классов с приватным конструктором.
Простейший Singleton — объект
Заголовок раздела «Простейший Singleton — объект»Если объект создаётся один раз — он уже Singleton:
// Простейший синглтон — объект-литералconst config = { host: 'localhost', port: 3000, debug: false,
get(key) { return this[key] }, set(key, value) { this[key] = value },}
config.set('debug', true)console.log(config.get('debug')) // true
// Это один и тот же объект везде в кодеimport { config } from './config.js'Singleton через Class
Заголовок раздела «Singleton через Class»class Database { static #instance = null // приватное статическое поле #connection = null
constructor(connectionString) { if (Database.#instance) { return Database.#instance // возвращаем существующий экземпляр }
// Первый вызов — инициализируем this.#connection = { url: connectionString, connected: false } Database.#instance = this }
connect() { this.#connection.connected = true console.log(`Подключено к ${this.#connection.url}`) return this }
query(sql) { if (!this.#connection.connected) throw new Error('Нет соединения') console.log(`Запрос: ${sql}`) return [] }
static getInstance() { if (!Database.#instance) { new Database('postgresql://localhost/mydb') } return Database.#instance }}
const db1 = new Database('postgresql://host1/db')const db2 = new Database('postgresql://host2/db') // возвращает тот же экземпляр!
console.log(db1 === db2) // true
// Рекомендуется через getInstanceconst db = Database.getInstance()db.connect().query('SELECT * FROM users')Singleton через IIFE и замыкание
Заголовок раздела «Singleton через IIFE и замыкание»const AppState = (function() { let instance
function createInstance() { const state = { user: null, theme: 'dark', language: 'ru', notifications: [], }
return { get(key) { return state[key] }, set(key, value) { state[key] = value; return this }, addNotification(msg) { state.notifications.push({ msg, time: Date.now() }) return this }, getNotifications() { return [...state.notifications] }, } }
return { getInstance() { if (!instance) instance = createInstance() return instance } }})()
const state1 = AppState.getInstance()const state2 = AppState.getInstance()
state1.set('user', { name: 'Алексей' }).addNotification('Добро пожаловать!')console.log(state2.get('user').name) // 'Алексей' — тот же объект!console.log(state1 === state2) // trueLazy Singleton
Заголовок раздела «Lazy Singleton»Создаётся только при первом обращении:
class ServiceLocator { static #services = new Map()
static register(name, factory) { this.#services.set(name, { factory, instance: null }) }
static get(name) { const service = this.#services.get(name) if (!service) throw new Error(`Сервис "${name}" не зарегистрирован`)
// Lazy: создаём экземпляр только при первом запросе if (!service.instance) { service.instance = service.factory() } return service.instance }}
// Регистрируем сервисыServiceLocator.register('logger', () => ({ log: (msg) => console.log(`[LOG] ${msg}`)}))
ServiceLocator.register('cache', () => { const store = new Map() return { set: (k, v) => store.set(k, v), get: (k) => store.get(k), }})
// Используемconst logger = ServiceLocator.get('logger')logger.log('Привет!')
const cache1 = ServiceLocator.get('cache')const cache2 = ServiceLocator.get('cache')cache1.set('key', 'value')console.log(cache2.get('key')) // 'value' — тот же экземплярconsole.log(cache1 === cache2) // trueModule Singleton (ES Modules)
Заголовок раздела «Module Singleton (ES Modules)»В ES Modules каждый файл — уже Singleton по умолчанию:
// store.js — выполняется только один раз при первом импортеlet _state = { count: 0, user: null,}
export function getState() { return { ..._state } }export function setState(updates) { Object.assign(_state, updates) }export function increment() { _state.count++ }
// Любой файл, который импортирует store.js, получает тот же экземпляр// import { getState, setState } from './store.js'Когда использовать Singleton
Заголовок раздела «Когда использовать Singleton»✅ Подходит:
- Конфигурация приложения
- Пул соединений с БД
- Logger/система логирования
- Кэш
- EventBus / глобальный шина событий
❌ Избегать:
- Если состояние должно быть независимым (тесты!)
- Как замена правильной архитектуре
- Когда создаёт скрытые зависимости
Тестирование: Синглтоны усложняют тестирование. Используйте Dependency Injection или позвольте сбрасывать экземпляр между тестами.