Как реализовать синглтон в JavaScript
В этой статье мы рассмотрим, что такое синглтон и все способы его реализации в JavaScript.
Синглтон — это шаблон проектирования, решающий две проблемы. Это гарантирует, что однопоточное приложение будет иметь только один экземпляр данного класса. Вторая проблема, когда нам нужно иметь глобальную точку доступа к какому-то экземпляру класса или просто объекту. Иногда разработчики называют некоторый объект синглтоном, даже если он решает только одну проблему.
Вот несколько случаев, когда мы можем использовать синглтон:
- Глобальное состояние приложения как единственный источник истинного — Redux, менеджеры состояний Mobx обеспечивают глобальный доступ к состоянию
- Служба API, в которой мы инкапсулируем некоторую логику и сохраняем сеанс или токен для выполнения вызовов API.
- Служба для хранения базы данных или соединения WebSocket, и здесь мы можем быть уверены, что у нас есть только одно соединение данного синглтона.
Итак, давайте посмотрим, как мы можем реализовать следующий шаблон.
Модуль экспорта/импорта JavaScript
Сам по себе класс Singleton не является сущностью или функцией JavaScript — это подход или шаблон, который мы каким-то образом реализуем. Все может зависеть от проекта, стека технологий, который мы используем, и даже навыков разработчика.
Как правило, вы можете договориться, когда использовать конкретный класс в качестве единственного экземпляра без какой-либо конкретной защиты от дублирования. Это может хорошо работать, особенно когда вы работаете в одиночку или с небольшой командой. Но в больших командах и проектах вы должны знать об этом и создавать свои синглтоны без возможности иметь более одного экземпляра класса.
Самый примитивный и простой подход к реализации синглтона в JavaScript — это экспорт вашего экземпляра из модуля с использованием export/import
синтаксиса:
class ApiService {}
export const API = new ApiService();
// and then somewhere in other files:
import {API} from './api';
Файл может содержать переменные, логику и методы, которые можно экспортировать как готовые к использованию объекты. Когда нам нужен такой объект, мы просто импортируем и используем его. Это хорошо работает, но в больших проектах может быть сложно управлять поддельными экземплярами сервисов для тестов, или кто-то может импортировать необработанный класс ApiService и создать второй экземпляр.
Несмотря на свою примитивность, этот подход до сих пор используется в большинстве проектов.
Синглтон на основе класса
Здесь мы реализуем одноэлементный класс, который всегда будет возвращать один и тот же экземпляр, а не создавать новый. Из-за того, что по замыслу всегда ожидается, что вызовы конструктора будут возвращать новые объекты, такое поведение невозможно реализовать с помощью обычного конструктора.
Но что, если мы вернем что-то из конструктора? Давайте взглянем:
class Store {
someValue = 'foo'
constructor() {
return {
anotherValue: 'bar'
}
}
}
const store = new Store()
console.log(store.someValue) // undefined
console.log(store.anotherValue) // bar
Как мы видим, если мы вернем объект из конструктора, этот объект будет возвращен как экземпляр класса.
Используя этот трюк, мы можем определить и сохранить экземпляр в поле класса и вернуть его, если это значение уже существует:
class WebSocketService {
constructor() {
if (WebSocketService._instance) {
return WebSocketService._instance;
}
WebSocketService._instance = this;
}
}
const socket1 = new WebSocketService();
const socket2 = new WebSocketService();
console.log(socket1 === socket2); // true
Синглтон на основе класса с экземпляром в закрытии функции
Предыдущий пример не самый правильный и чистый, так как _instance
поле остается доступным и мы можем переопределить его вручную.
Давайте рассмотрим наиболее эффективный способ реализации синглтона. Подход заключается в создании функции-оболочки, из которой мы будем возвращать наш класс, а при закрытии этой функции мы создадим наше поле экземпляра:
const GlobalStore = (() => {
let instance = null;
return class GlobalStore {
constructor() {
if(instance === null) {
instance = this;
}
return instance;
}
}
})();
В этом примере мы помещаем экземпляр в замыкание функции. Затем в конструкторе класса мы проверяем, есть ли у нас экземпляр класса, и, если нет, присваиваем экземпляру значение и возвращаем его.
Также вы можете заметить, что функция-оболочка создается и сразу же вызывается, таким образом, мы создали замыкание, недоступное извне, и класс может легко читать и переопределять значения из замыкания.
Бонусом такого подхода является то, что вы можете добавить в замыкание любую переменную или логику, в результате чего вы получите настоящие приватные методы, к которым вы не сможете получить доступ из экземпляра.