DevGang
Авторизоваться

Как реализовать синглтон в 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;
    }
  }
})();

В этом примере мы помещаем экземпляр в замыкание функции. Затем в конструкторе класса мы проверяем, есть ли у нас экземпляр класса, и, если нет, присваиваем экземпляру значение и возвращаем его.

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

Бонусом такого подхода является то, что вы можете добавить в замыкание любую переменную или логику, в результате чего вы получите настоящие приватные методы, к которым вы не сможете получить доступ из экземпляра.

#JavaScript
Комментарии 1
Игорь Кузнецов 01.07.2023 в 14:00

Третий пример непонятно как использовать.

Чтобы оставить комментарий, необходимо авторизоваться