Back

Синглтоны в JavaScript: Полезный инструмент или скрытая ловушка?

Синглтоны в JavaScript: Полезный инструмент или скрытая ловушка?

Вы написали модуль, который экспортирует настроенный логгер или экземпляр API-клиента. Каждый файл импортирует его, и вы предполагаете, что во всём приложении работает ровно один экземпляр. Затем ваши тесты начинают «протекать» состоянием между запусками. Или ваша микрофронтенд-архитектура внезапно получает две копии вашего «синглтона», конфликтующие друг с другом. Что произошло?

Паттерн синглтон в JavaScript работает не так, как предполагает классическая теория проектирования. Понимание того, где ES-модульные синглтоны фактически существуют — и где они ломаются — избавит вас от отладочных сессий, похожих на охоту за призраками.

Ключевые выводы

  • ES-модульные синглтоны кешируются для каждого графа модулей и среды выполнения, а не глобально по всей системе
  • Синглтоны хорошо работают для неизменяемых данных и операций без состояния, таких как утилиты логирования и конфигурация только для чтения
  • Изменяемое состояние в синглтонах становится опасным при серверном рендеринге, в тестовых окружениях и микрофронтенд-архитектурах
  • Перед использованием синглтона подумайте, выполняется ли ваш код в нескольких бандлах, воркерах или серверных запросах, и нужно ли тестам сбрасывать экземпляр

Что на самом деле означает «синглтон» в современном JavaScript

Забудьте на мгновение об определении «Банды четырёх». В современном JavaScript синглтон — это обычно просто модуль, который экспортирует уже созданный объект:

// logger.js
class Logger {
  log(message) {
    console.log(`[${Date.now()}] ${message}`)
  }
}

export const logger = new Logger()

Когда вы импортируете logger из нескольких файлов, вы получаете один и тот же экземпляр — не из-за хитрых трюков с конструктором, а потому что ES-модули кешируются для каждого графа модулей и среды выполнения. Модуль выполняется один раз, экземпляр создаётся один раз, и каждый импорт получает ссылку на этот же объект.

Это основа ES-модульных синглтонов. Это просто и часто именно то, что вам нужно.

Предположение, которое ломается

Вот где возникают подводные камни синглтонов в JavaScript: этот «единственный экземпляр» ограничен конкретным графом модулей в конкретной среде выполнения JavaScript. Он не является магически глобальным для всей вашей системы.

Это предположение ломается в нескольких реальных сценариях:

Множественные бандлы или дублирующиеся пакеты. Если в вашем монорепозитории два пакета, каждый из которых бандлит свою копию зависимости, вы получаете два отдельных графа модулей. Два «синглтона». Ваше общее состояние больше не является общим.

Тест-раннеры. Jest, Vitest и подобные инструменты часто сбрасывают кеш модулей между тестовыми файлами или используют рабочие процессы. Ваш синглтон из одного тестового файла может быть не тем же экземпляром в другом.

Микрофронтенды. Каждый независимо развёрнутый фронтенд обычно имеет свою среду выполнения JavaScript. Синглтон в одном микрофронтенде невидим для другого, если экземпляры не используются совместно явно через сборку или среду выполнения.

Web Workers и Service Workers. Они работают в отдельных контекстах JavaScript. Модуль, импортированный в воркере, — это совершенно другой экземпляр по сравнению с тем же модулем в основном потоке.

Серверные среды выполнения с изоляцией запросов. В фреймворках вроде Next.js или Nuxt, работающих на сервере, синглтон может сохраняться между несколькими пользовательскими запросами в зависимости от среды выполнения и модели развёртывания, создавая риск утечки данных, если он содержит изменяемое состояние, специфичное для запроса.

Где синглтоны работают хорошо

Несмотря на эти подводные камни, ES-модульные синглтоны остаются действительно полезными для определённых фронтенд-утилит и паттернов координации:

  • Утилиты логирования. Общий логгер с единообразным форматированием не причиняет вреда при дублировании и не содержит чувствительного состояния для каждого запроса.
  • Снимки конфигурации. Конфигурация только для чтения, загруженная при запуске, отлично работает как синглтон. Ключевое слово — только для чтения.
  • Утилиты без состояния. Вспомогательные функции или классы, которые не поддерживают изменяемое состояние между вызовами, являются безопасными кандидатами.

Общая нить: эти синглтоны либо содержат неизменяемые данные, либо выполняют операции без состояния.

Где синглтоны становятся обузой

Изменяемое состояние — вот где всё становится опасным. Рассмотрим:

  • Данные пользовательской сессии. При серверном рендеринге синглтон, содержащий информацию о пользователе, может «протекать» между запросами.
  • Кеши, привязанные к запросу. Данные, которые должны сбрасываться для каждого запроса, неправильно сохраняются.
  • Общая изменяемая конфигурация. Одна часть вашего приложения изменяет настройки, неожиданно влияя на другую.

Современные инструменты усиливают эти проблемы. В React 18+ Strict Mode намеренно дважды вызывает рендеры и определённые эффекты в режиме разработки, обнажая состояние синглтона, которое не изолировано должным образом. Hot Module Replacement в Vite или webpack может сохранять состояние синглтона между изменениями кода, создавая тонкие баги, где ваш «свежий» код работает с устаревшими данными.

Практический подход

Паттерн синглтон в JavaScript не является плохим по своей сути — он просто более узкий, чем предполагают многие разработчики. Прежде чем использовать экземпляр на уровне модуля, спросите себя:

  1. Действительно ли это состояние неизменяемо или не имеет состояния?
  2. Может ли этот код выполняться в нескольких бандлах, воркерах или серверных запросах?
  3. Нужно ли моим тестам сбрасывать или мокировать этот экземпляр?

Если вы отвечаете «да» на вопросы 2 или 3 с изменяемым состоянием, рассмотрите альтернативы: фабричные функции, внедрение зависимостей или специфичные для фреймворка паттерны, такие как React Context или сервисы, привязанные к запросу.

Заключение

Синглтоны — полезный инструмент, когда вы понимаете их фактическую область действия. Они становятся скрытой ловушкой, когда вы предполагаете, что «единственный экземпляр» означает что-то, чего среда выполнения никогда не обещала. Придерживайтесь неизменяемых данных или данных без состояния для ваших экземпляров на уровне модуля и используйте внедрение зависимостей или фабричные паттерны, когда вам нужна правильная изоляция между тестами, запросами или границами среды выполнения.

Часто задаваемые вопросы

Jest часто сбрасывает кеш модулей между тестовыми файлами или запускает тесты в изолированных рабочих процессах, создавая свежие графы модулей и переинстанцируя ваш синглтон. Используйте jest.resetModules() или jest.isolateModules() где это уместно, или избегайте изменяемого состояния на уровне модуля, внедряя зависимости.

Нет. Web Workers работают в отдельных контекстах JavaScript со своими собственными графами модулей. Модуль, импортированный в воркере, — это совершенно другой экземпляр по сравнению с тем же модулем в основном потоке. Чтобы разделить состояние, вы должны явно передавать данные между контекстами, используя postMessage или SharedArrayBuffer.

Только для неизменяемых данных или данных без состояния. Синглтоны на сервере могут сохраняться между несколькими пользовательскими запросами в зависимости от среды выполнения и модели развёртывания, потенциально допуская утечку чувствительных данных между пользователями. Для состояния, специфичного для запроса, такого как пользовательские сессии или кеши, используйте паттерны, привязанные к запросу, предоставляемые вашим фреймворком, вместо экземпляров на уровне модуля.

Фабричные функции позволяют создавать свежие экземпляры по требованию. Внедрение зависимостей передаёт экземпляры явно, облегчая тестирование. Специфичные для фреймворка решения, такие как React Context, обеспечивают управление состоянием с определённой областью действия. Для серверных приложений сервисы, привязанные к запросу, обеспечивают правильную изоляцию между запросами, сохраняя при этом удобство общих утилит.

Complete picture for complete understanding

Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.

Check our GitHub repo and join the thousands of developers in our community.

OpenReplay