> Как улучшить реализацию EventEmitter с использованием Map и Set в JavaScript (JavaScript)

Уровень: senior · Роль: frontend · Категория: Технические вопросы

Компании: Т-Банк

Стек: JavaScript

> Пример ответа

Короткий ответ

Использование Map для хранения событий и Set для уникальных обработчиков улучшает EventEmitter за счёт гарантии O(1) сложности операций добавления, удаления и поиска, а также автоматического предотвращения дублирования колбэков. Map обеспечивает эффективное управление событиями по именам, а Set - быструю проверку существования обработчика и его удаление без лишних итераций.

Подробное объяснение

Традиционная реализация EventEmitter на объектах ({}) имеет несколько недостатков: ключи могут быть только строками, нет встроенной защиты от дублирования обработчиков, а удаление требует полного перебора массива. Использование Map решает проблему ограничения типов ключей - теперь можно использовать любые значения (символы, объекты) в качестве имён событий. Set вместо массива даёт гарантию уникальности обработчиков на уровне структуры данных: при повторном добавлении одного и того же колбэка он не дублируется. Это критично для сложных систем с множеством подписок, где дублирование может привести к утечкам памяти и неожиданному поведению.

Map также предоставляет удобные методы для управления жизненным циклом событий: has, delete, clear работают за O(1). При удалении события можно просто вызвать map.delete(eventName), что автоматически очищает все обработчики. В комбинации с WeakMap для хранения контекста можно избежать утечек памяти при работе с временными объектами.

На практике

При реализации EventEmitter на Map и Set важно учитывать порядок вызова обработчиков - Set сохраняет порядок вставки, что гарантирует предсказуемое выполнение. Для массового удаления подписок удобно использовать Map.prototype.clear() для конкретного события или WeakRef для автоматической очистки при сборке мусора. В современных фреймворках (например, в Node.js EventEmitter) такая реализация даёт прирост производительности при частых подписках/отписках.

Пример кода

JAVASCRIPT
class EventEmitter {
constructor() {
this._events = new Map();
}
on(eventName, handler) {
if (!this._events.has(eventName)) {
this._events.set(eventName, new Set());
}
this._events.get(eventName).add(handler);
return () => this.off(eventName, handler);
}
off(eventName, handler) {
const handlers = this._events.get(eventName);
if (handlers) {
handlers.delete(handler);
if (handlers.size === 0) {
this._events.delete(eventName);
}
}
}
emit(eventName, ...args) {
const handlers = this._events.get(eventName);
if (handlers) {
handlers.forEach(handler => handler(...args));
}
}
once(eventName, handler) {
const wrapper = (...args) => {
handler(...args);
this.off(eventName, wrapper);
};
this.on(eventName, wrapper);
}
removeAllListeners(eventName) {
if (eventName) {
this._events.delete(eventName);
} else {
this._events.clear();
}
}
}

Как отвечать на собеседовании

Начните с объяснения недостатков классической реализации на объектах и массивах. Затем покажите, как Map решает проблему типов ключей, а Set - проблему дублирования. Упомяните сложность операций: O(1) для Map.get/set/delete и O(1) для Set.add/delete/has. Приведите пример с WeakMap для предотвращения утечек памяти. Если спросят про производительность, скажите, что Set быстрее массива при удалении, так как не требует поиска по индексу.

Что проверяет интервьюер

Интервьюер оценивает понимание структур данных в JavaScript, умение выбирать правильные коллекции под задачу, знание нюансов работы Set (порядок вставки, сравнение по ссылке) и Map (любые ключи). Также проверяется способность проектировать API с учётом производительности и избегать типичных ошибок (например, мутация коллекции во время итерации). Для senior-уровня важно показать понимание memory management и garbage collection.

Типичные ошибки

  • Использование Map без Set (остаётся проблема дублирования при ручном управлении массивами)
  • Забывание удалить пустой Set из Map (приводит к утечке памяти)
  • Мутация Set во время forEach (если обработчик вызывает off, это ломает итерацию)
  • Неправильная обработка this в колбэках (нужно либо bind, либо стрелочные функции)
  • Игнорирование возврата функции отписки из on (удобный паттерн для React useEffect)

> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?

Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью