Паттерны проектирования — это проверенные решения типичных задач. В JS наиболее востребованы: Observer/EventEmitter (подписки на события), Singleton (один экземпляр), Factory (фабрика объектов), Decorator (расширение поведения), Strategy (замена алгоритмов), Module (инкапсуляция через замыкания). JS позволяет реализовывать паттерны лаконично благодаря замыканиям и прототипному наследованию.
Один из самых важных паттернов в JS. Позволяет объектам подписываться на события и реагировать на изменения.
class EventEmitter {
constructor() {
this._events = {}
}
// Подписка на событие
on(event, listener) {
if (!this._events[event]) {
this._events[event] = []
}
this._events[event].push(listener)
return this // для цепочки вызовов
}
// Отписка от события
off(event, listener) {
if (!this._events[event]) return this
this._events[event] = this._events[event].filter(l => l !== listener)
return this
}
// Подписка только на одно срабатывание
once(event, listener) {
const wrapper = (...args) => {
listener(...args)
this.off(event, wrapper)
}
return this.on(event, wrapper)
}
// Генерация события
emit(event, ...args) {
if (!this._events[event]) return false
this._events[event].forEach(listener => listener(...args))
return true
}
}
// Использование
const emitter = new EventEmitter()
emitter.on('data', (payload) => console.log('Получено:', payload))
emitter.emit('data', { id: 1, value: 42 }) // 'Получено: {id: 1, value: 42}'Гарантирует единственный экземпляр класса. В JS реализуется через замыкание или статическое свойство.
// Через замыкание (модульный паттерн)
const AppConfig = (() => {
let instance = null
function createInstance() {
return {
theme: 'light',
language: 'ru',
apiUrl: 'https://api.example.com'
}
}
return {
getInstance() {
if (!instance) {
instance = createInstance()
}
return instance
}
}
})()
const config1 = AppConfig.getInstance()
const config2 = AppConfig.getInstance()
console.log(config1 === config2) // true — один и тот же объект
// Через класс со статическим свойством
class Database {
static #instance = null
constructor(url) {
if (Database.#instance) {
return Database.#instance
}
this.url = url
this.connected = false
Database.#instance = this
}
static getInstance(url) {
if (!Database.#instance) {
new Database(url)
}
return Database.#instance
}
}Создаёт объекты без указания их конкретного класса. Инкапсулирует логику создания.
// Простая фабрика
function createUser(type, name) {
const base = {
name,
createdAt: new Date().toISOString()
}
const roles = {
admin: { ...base, permissions: ['read', 'write', 'delete'], role: 'admin' },
editor: { ...base, permissions: ['read', 'write'], role: 'editor' },
viewer: { ...base, permissions: ['read'], role: 'viewer' }
}
if (!roles[type]) throw new Error('Unknown user type: ' + type)
return roles[type]
}
const admin = createUser('admin', 'Алиса')
const viewer = createUser('viewer', 'Боб')Инкапсулирует состояние и поведение, предоставляет публичный API через замыкание.
const Counter = (() => {
let count = 0 // приватное состояние
return {
increment() { count++ },
decrement() { count-- },
reset() { count = 0 },
getValue() { return count }
// count недоступен напрямую извне!
}
})()
Counter.increment()
Counter.increment()
Counter.getValue() // 2
// Counter.count // undefined — инкапсулированоДобавляет поведение объекту/функции без изменения оригинала.
// Декоратор функции — добавляет логирование
function withLogging(fn, name = fn.name) {
return function(...args) {
console.log(`[${name}] вызов с:'`, args)
const result = fn.apply(this, args)
console.log(`[${name}] результат:'`, result)
return result
}
}
const add = (a, b) => a + b
const loggedAdd = withLogging(add)
loggedAdd(2, 3) // [add] вызов с: [2, 3]; [add] результат: 5Определяет семейство алгоритмов, инкапсулирует каждый, делает их взаимозаменяемыми.
// Сортировка с разными стратегиями
const sortStrategies = {
bubble(arr) {
const a = [...arr]
for (let i = 0; i < a.length - 1; i++) {
for (let j = 0; j < a.length - i - 1; j++) {
if (a[j] > a[j + 1]) [a[j], a[j + 1]] = [a[j + 1], a[j]]
}
}
return a
},
quick(arr) {
if (arr.length <= 1) return arr
const pivot = arr[Math.floor(arr.length / 2)]
const left = arr.filter(x => x < pivot)
const mid = arr.filter(x => x === pivot)
const right = arr.filter(x => x > pivot)
return [...sortStrategies.quick(left), ...mid, ...sortStrategies.quick(right)]
}
}
class Sorter {
constructor(strategy = 'quick') {
this.strategy = sortStrategies[strategy]
}
setStrategy(name) { this.strategy = sortStrategies[name] }
sort(arr) { return this.strategy(arr) }
}Не нужно перечислять все 23 паттерна GoF. Сосредоточься на трёх-четырёх, которые реально использовал: Observer, Singleton, Factory, Decorator. Для каждого: назови проблему → покажи паттерн → объясни преимущества. Упомяни конкретный пример из реального кода (EventEmitter, кэширование через Singleton).
Реализация EventEmitter, Singleton через замыкание, Factory, и практический пример их совместного использования
// ===== EVENT EMITTER (Observer) =====
console.log('=== EventEmitter ===')
class EventEmitter {
constructor() {
this._events = {}
this._onceWrappers = new WeakMap() // для корректной отписки once
}
on(event, listener) {
(this._events[event] ??= []).push(listener)
return this
}
off(event, listener) {
if (!this._events[event]) return this
this._events[event] = this._events[event].filter(l => l !== listener)
return this
}
once(event, listener) {
const wrapper = (...args) => {
listener(...args)
this.off(event, wrapper)
}
this._onceWrappers.set(listener, wrapper)
return this.on(event, wrapper)
}
emit(event, ...args) {
const listeners = this._events[event]
if (!listeners?.length) return false
listeners.slice().forEach(l => l(...args))
return true
}
listenerCount(event) {
return this._events[event]?.length ?? 0
}
}
// Пример: Store на основе EventEmitter
class Store extends EventEmitter {
constructor(initialState) {
super()
this._state = initialState
}
getState() { return { ...this._state } }
setState(updates) {
const prevState = this._state
this._state = { ...this._state, ...updates }
this.emit('change', this._state, prevState)
}
}
const store = new Store({ count: 0, user: null })
// Подписка на изменения
const unsubscribeLog = (newState) => {
console.log('Изменение состояния:', newState)
}
store.on('change', unsubscribeLog)
// Подписка once — сработает только 1 раз
store.once('change', (state) => {
console.log('Первое изменение (once):', state.count)
})
store.setState({ count: 1 }) // оба listener сработают
store.setState({ count: 2 }) // только unsubscribeLog
console.log('Listeners после двух изменений:', store.listenerCount('change')) // 1
// ===== SINGLETON =====
console.log('\n=== Singleton ===')
const Logger = (() => {
let instance = null
let logCount = 0
class LoggerClass {
constructor(prefix) {
if (instance) return instance // возвращаем существующий
this.prefix = prefix
this.logs = []
instance = this
}
log(level, message) {
logCount++
const entry = {
id: logCount,
level,
message,
prefix: this.prefix,
timestamp: new Date().toISOString()
}
this.logs.push(entry)
console.log(`[${this.prefix}][${level.toUpperCase()}] ${message}`)
}
info(msg) { this.log('info', msg) }
warn(msg) { this.log('warn', msg) }
error(msg) { this.log('error', msg) }
getStats() {
return {
total: this.logs.length,
byLevel: this.logs.reduce((acc, l) => {
acc[l.level] = (acc[l.level] || 0) + 1
return acc
}, {})
}
}
}
return LoggerClass
})()
const logger1 = new Logger('APP')
const logger2 = new Logger('ДРУГОЙ') // вернёт logger1!
logger1.info('Приложение запущено')
logger2.warn('Это пишет тот же экземпляр')
logger1.error('Ошибка подключения')
console.log('logger1 === logger2:', logger1 === logger2) // true
console.log('Статистика:', logger1.getStats())
// ===== FACTORY =====
console.log('\n=== Factory ===')
function createNotification(type, data) {
const base = {
id: Math.random().toString(36).slice(2),
createdAt: Date.now(),
read: false,
markRead() { this.read = true }
}
const templates = {
success: {
...base,
type: 'success',
icon: 'check',
title: data.title || 'Успешно',
message: data.message,
duration: data.duration || 3000
},
error: {
...base,
type: 'error',
icon: 'x',
title: data.title || 'Ошибка',
message: data.message,
duration: data.duration || 0, // ошибки не исчезают автоматически
retry: data.retry || null
},
info: {
...base,
type: 'info',
icon: 'i',
title: data.title || 'Информация',
message: data.message,
duration: data.duration || 5000
}
}
if (!templates[type]) {
throw new Error(`Неизвестный тип уведомления: ${type}`)
}
return templates[type]
}
const successNotification = createNotification('success', {
message: 'Файл успешно загружен'
})
const errorNotification = createNotification('error', {
title: 'Ошибка сети',
message: 'Не удалось подключиться к серверу',
retry: () => console.log('Повтор...')
})
console.log('Success:', successNotification.title, '-', successNotification.message)
console.log('Error:', errorNotification.title, '| duration:', errorNotification.duration)
console.log('Error id:', errorNotification.id)
// ===== ПАТТЕРНЫ ВМЕСТЕ: УВЕДОМЛЕНИЯ С EVENTЕМITTER =====
console.log('\n=== Объединяем: NotificationService ===')
class NotificationService extends EventEmitter {
constructor() {
super()
this.notifications = []
}
add(type, data) {
const notification = createNotification(type, data)
this.notifications.push(notification)
this.emit('added', notification)
if (notification.duration > 0) {
setTimeout(() => this.remove(notification.id), notification.duration)
}
return notification
}
remove(id) {
const index = this.notifications.findIndex(n => n.id === id)
if (index !== -1) {
const [removed] = this.notifications.splice(index, 1)
this.emit('removed', removed)
}
}
getAll() { return [...this.notifications] }
}
// Singleton — один сервис уведомлений на всё приложение
const notificationSingleton = (() => {
let service = null
return {
getInstance() {
if (!service) service = new NotificationService()
return service
}
}
})()
const ns = notificationSingleton.getInstance()
ns.on('added', n => console.log('+ Уведомление добавлено:', n.type, '-', n.message))
ns.on('removed', n => console.log('- Уведомление удалено:', n.id))
ns.add('success', { message: 'Сохранено' })
ns.add('info', { message: 'Новое сообщение от Алисы' })
ns.add('error', { message: 'Ошибка загрузки' })
console.log('Всего уведомлений:', ns.getAll().length)Паттерны проектирования — это проверенные решения типичных задач. В JS наиболее востребованы: Observer/EventEmitter (подписки на события), Singleton (один экземпляр), Factory (фабрика объектов), Decorator (расширение поведения), Strategy (замена алгоритмов), Module (инкапсуляция через замыкания). JS позволяет реализовывать паттерны лаконично благодаря замыканиям и прототипному наследованию.
Один из самых важных паттернов в JS. Позволяет объектам подписываться на события и реагировать на изменения.
class EventEmitter {
constructor() {
this._events = {}
}
// Подписка на событие
on(event, listener) {
if (!this._events[event]) {
this._events[event] = []
}
this._events[event].push(listener)
return this // для цепочки вызовов
}
// Отписка от события
off(event, listener) {
if (!this._events[event]) return this
this._events[event] = this._events[event].filter(l => l !== listener)
return this
}
// Подписка только на одно срабатывание
once(event, listener) {
const wrapper = (...args) => {
listener(...args)
this.off(event, wrapper)
}
return this.on(event, wrapper)
}
// Генерация события
emit(event, ...args) {
if (!this._events[event]) return false
this._events[event].forEach(listener => listener(...args))
return true
}
}
// Использование
const emitter = new EventEmitter()
emitter.on('data', (payload) => console.log('Получено:', payload))
emitter.emit('data', { id: 1, value: 42 }) // 'Получено: {id: 1, value: 42}'Гарантирует единственный экземпляр класса. В JS реализуется через замыкание или статическое свойство.
// Через замыкание (модульный паттерн)
const AppConfig = (() => {
let instance = null
function createInstance() {
return {
theme: 'light',
language: 'ru',
apiUrl: 'https://api.example.com'
}
}
return {
getInstance() {
if (!instance) {
instance = createInstance()
}
return instance
}
}
})()
const config1 = AppConfig.getInstance()
const config2 = AppConfig.getInstance()
console.log(config1 === config2) // true — один и тот же объект
// Через класс со статическим свойством
class Database {
static #instance = null
constructor(url) {
if (Database.#instance) {
return Database.#instance
}
this.url = url
this.connected = false
Database.#instance = this
}
static getInstance(url) {
if (!Database.#instance) {
new Database(url)
}
return Database.#instance
}
}Создаёт объекты без указания их конкретного класса. Инкапсулирует логику создания.
// Простая фабрика
function createUser(type, name) {
const base = {
name,
createdAt: new Date().toISOString()
}
const roles = {
admin: { ...base, permissions: ['read', 'write', 'delete'], role: 'admin' },
editor: { ...base, permissions: ['read', 'write'], role: 'editor' },
viewer: { ...base, permissions: ['read'], role: 'viewer' }
}
if (!roles[type]) throw new Error('Unknown user type: ' + type)
return roles[type]
}
const admin = createUser('admin', 'Алиса')
const viewer = createUser('viewer', 'Боб')Инкапсулирует состояние и поведение, предоставляет публичный API через замыкание.
const Counter = (() => {
let count = 0 // приватное состояние
return {
increment() { count++ },
decrement() { count-- },
reset() { count = 0 },
getValue() { return count }
// count недоступен напрямую извне!
}
})()
Counter.increment()
Counter.increment()
Counter.getValue() // 2
// Counter.count // undefined — инкапсулированоДобавляет поведение объекту/функции без изменения оригинала.
// Декоратор функции — добавляет логирование
function withLogging(fn, name = fn.name) {
return function(...args) {
console.log(`[${name}] вызов с:'`, args)
const result = fn.apply(this, args)
console.log(`[${name}] результат:'`, result)
return result
}
}
const add = (a, b) => a + b
const loggedAdd = withLogging(add)
loggedAdd(2, 3) // [add] вызов с: [2, 3]; [add] результат: 5Определяет семейство алгоритмов, инкапсулирует каждый, делает их взаимозаменяемыми.
// Сортировка с разными стратегиями
const sortStrategies = {
bubble(arr) {
const a = [...arr]
for (let i = 0; i < a.length - 1; i++) {
for (let j = 0; j < a.length - i - 1; j++) {
if (a[j] > a[j + 1]) [a[j], a[j + 1]] = [a[j + 1], a[j]]
}
}
return a
},
quick(arr) {
if (arr.length <= 1) return arr
const pivot = arr[Math.floor(arr.length / 2)]
const left = arr.filter(x => x < pivot)
const mid = arr.filter(x => x === pivot)
const right = arr.filter(x => x > pivot)
return [...sortStrategies.quick(left), ...mid, ...sortStrategies.quick(right)]
}
}
class Sorter {
constructor(strategy = 'quick') {
this.strategy = sortStrategies[strategy]
}
setStrategy(name) { this.strategy = sortStrategies[name] }
sort(arr) { return this.strategy(arr) }
}Не нужно перечислять все 23 паттерна GoF. Сосредоточься на трёх-четырёх, которые реально использовал: Observer, Singleton, Factory, Decorator. Для каждого: назови проблему → покажи паттерн → объясни преимущества. Упомяни конкретный пример из реального кода (EventEmitter, кэширование через Singleton).
Реализация EventEmitter, Singleton через замыкание, Factory, и практический пример их совместного использования
// ===== EVENT EMITTER (Observer) =====
console.log('=== EventEmitter ===')
class EventEmitter {
constructor() {
this._events = {}
this._onceWrappers = new WeakMap() // для корректной отписки once
}
on(event, listener) {
(this._events[event] ??= []).push(listener)
return this
}
off(event, listener) {
if (!this._events[event]) return this
this._events[event] = this._events[event].filter(l => l !== listener)
return this
}
once(event, listener) {
const wrapper = (...args) => {
listener(...args)
this.off(event, wrapper)
}
this._onceWrappers.set(listener, wrapper)
return this.on(event, wrapper)
}
emit(event, ...args) {
const listeners = this._events[event]
if (!listeners?.length) return false
listeners.slice().forEach(l => l(...args))
return true
}
listenerCount(event) {
return this._events[event]?.length ?? 0
}
}
// Пример: Store на основе EventEmitter
class Store extends EventEmitter {
constructor(initialState) {
super()
this._state = initialState
}
getState() { return { ...this._state } }
setState(updates) {
const prevState = this._state
this._state = { ...this._state, ...updates }
this.emit('change', this._state, prevState)
}
}
const store = new Store({ count: 0, user: null })
// Подписка на изменения
const unsubscribeLog = (newState) => {
console.log('Изменение состояния:', newState)
}
store.on('change', unsubscribeLog)
// Подписка once — сработает только 1 раз
store.once('change', (state) => {
console.log('Первое изменение (once):', state.count)
})
store.setState({ count: 1 }) // оба listener сработают
store.setState({ count: 2 }) // только unsubscribeLog
console.log('Listeners после двух изменений:', store.listenerCount('change')) // 1
// ===== SINGLETON =====
console.log('\n=== Singleton ===')
const Logger = (() => {
let instance = null
let logCount = 0
class LoggerClass {
constructor(prefix) {
if (instance) return instance // возвращаем существующий
this.prefix = prefix
this.logs = []
instance = this
}
log(level, message) {
logCount++
const entry = {
id: logCount,
level,
message,
prefix: this.prefix,
timestamp: new Date().toISOString()
}
this.logs.push(entry)
console.log(`[${this.prefix}][${level.toUpperCase()}] ${message}`)
}
info(msg) { this.log('info', msg) }
warn(msg) { this.log('warn', msg) }
error(msg) { this.log('error', msg) }
getStats() {
return {
total: this.logs.length,
byLevel: this.logs.reduce((acc, l) => {
acc[l.level] = (acc[l.level] || 0) + 1
return acc
}, {})
}
}
}
return LoggerClass
})()
const logger1 = new Logger('APP')
const logger2 = new Logger('ДРУГОЙ') // вернёт logger1!
logger1.info('Приложение запущено')
logger2.warn('Это пишет тот же экземпляр')
logger1.error('Ошибка подключения')
console.log('logger1 === logger2:', logger1 === logger2) // true
console.log('Статистика:', logger1.getStats())
// ===== FACTORY =====
console.log('\n=== Factory ===')
function createNotification(type, data) {
const base = {
id: Math.random().toString(36).slice(2),
createdAt: Date.now(),
read: false,
markRead() { this.read = true }
}
const templates = {
success: {
...base,
type: 'success',
icon: 'check',
title: data.title || 'Успешно',
message: data.message,
duration: data.duration || 3000
},
error: {
...base,
type: 'error',
icon: 'x',
title: data.title || 'Ошибка',
message: data.message,
duration: data.duration || 0, // ошибки не исчезают автоматически
retry: data.retry || null
},
info: {
...base,
type: 'info',
icon: 'i',
title: data.title || 'Информация',
message: data.message,
duration: data.duration || 5000
}
}
if (!templates[type]) {
throw new Error(`Неизвестный тип уведомления: ${type}`)
}
return templates[type]
}
const successNotification = createNotification('success', {
message: 'Файл успешно загружен'
})
const errorNotification = createNotification('error', {
title: 'Ошибка сети',
message: 'Не удалось подключиться к серверу',
retry: () => console.log('Повтор...')
})
console.log('Success:', successNotification.title, '-', successNotification.message)
console.log('Error:', errorNotification.title, '| duration:', errorNotification.duration)
console.log('Error id:', errorNotification.id)
// ===== ПАТТЕРНЫ ВМЕСТЕ: УВЕДОМЛЕНИЯ С EVENTЕМITTER =====
console.log('\n=== Объединяем: NotificationService ===')
class NotificationService extends EventEmitter {
constructor() {
super()
this.notifications = []
}
add(type, data) {
const notification = createNotification(type, data)
this.notifications.push(notification)
this.emit('added', notification)
if (notification.duration > 0) {
setTimeout(() => this.remove(notification.id), notification.duration)
}
return notification
}
remove(id) {
const index = this.notifications.findIndex(n => n.id === id)
if (index !== -1) {
const [removed] = this.notifications.splice(index, 1)
this.emit('removed', removed)
}
}
getAll() { return [...this.notifications] }
}
// Singleton — один сервис уведомлений на всё приложение
const notificationSingleton = (() => {
let service = null
return {
getInstance() {
if (!service) service = new NotificationService()
return service
}
}
})()
const ns = notificationSingleton.getInstance()
ns.on('added', n => console.log('+ Уведомление добавлено:', n.type, '-', n.message))
ns.on('removed', n => console.log('- Уведомление удалено:', n.id))
ns.add('success', { message: 'Сохранено' })
ns.add('info', { message: 'Новое сообщение от Алисы' })
ns.add('error', { message: 'Ошибка загрузки' })
console.log('Всего уведомлений:', ns.getAll().length)Реализуй EventEmitter с нуля. Класс должен иметь методы: on(event, listener) — подписка, off(event, listener) — отписка, once(event, listener) — подписка на одно срабатывание, emit(event, ...args) — генерация события. Все методы on/off/once должны возвращать this для цепочки вызовов.
on: this._events[event] ??= []; this._events[event].push(listener). off: фильтруй через .filter(l => l !== listener). once: создай wrapper = (...args) => { listener(...args); this.off(event, wrapper) }, затем this.on(event, wrapper). emit: this._events[event].slice().forEach(l => l(...args)).
Токены для AI-помощника закончились
Купи токены чтобы задавать вопросы AI прямо в уроке