Как стать автором
Обновить

Приватные конструкторы JavaScript

Время на прочтение3 мин
Количество просмотров5.2K

Введение

Месяц назад вышел новый стандарт ECMAScript 2022, который нам дал всё больше и больше ООП. На Хабре уже была статья про то, что нам представили в новом стандарте, но сегодня я хочу остановиться на том, что мы не получили. Приватные конструкторы в студию.

Разберём, что такое приватные конструкторы, зачем они нужны и попробуем создать полифил.

Кто такой приватный конструктор?

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

А зачем он собственно нужен?

Я, собственно, как всегда буду высасывать проблемы из пальца. Первая из таких проблем - сокрытие реализации конструктора:

class Fahrenheit {
	constructor(value) {
    this.#value = value;
  }

	#value;
}

Данный конструктор предполагает, что при создании класса мы будем передавать значение градусов в Фаренгейтах. Хмм... А что если мы хотим перевести Цильсии в Фаренгейты и создавать Фаренгейты только из Цельсий? Кому это нужно - не знаю, но тем не менее :D

Мы можем сделать так:

class Fahrenheit {
  constructor(value) {
    this.#value = value;
  }
  
  #value;
  
  static fromCelsius(value) {
		return new Fahrenheit(value * 9/5 + 32);
  }
}

Вот тут-то мы и попались. В данном случае мы можем как конструктор использовать, так и статический метод.

Другой вариант, когда у нас есть необходимость в использовании приватного конструктора, - это асинхронное создание экземпляра класса. Пруфов нет, но вы мне верьте.

Как найти выход?

Давайте попробуем реализовать приватный конструктор самостоятельно? В качестве инструмента мы будем использовать прокси-объект:

function privateConstructor(cls) {
  // Флаг, который отвечает за то, что конструктор был вызван через метод,
  // а не через оператор new
  let viaMethod = false;
  
  return new Proxy(cls, {
    // Вешаем обработчик на конструктор, чтобы в случае вызова через new
    // выдавать ошибку
    construct: (target, args) => {
      if (!viaMethod) {
        throw new Error('Cannot use "new" for private constructor');
      }
      
      return Reflect.construct(target, args);
    },
    
    // Вешаем обработчик на каждое свойство и каждый метод
    get: (target, key) => {
      let maybeMethod = Reflect.get(target, key);
      
      // Проверяем, метод это или свойство
      if (maybeMethod instanceof Function) {
        // Переключаем флаг вызова через метод
        viaMethod = true;
      }
      
      return maybeMethod;
    }
  });
}

В итоге у нас получилось что-то наподобие декоратора. Однако при такой реализации у нас есть проблема. Давайте рассмотрим её.

Пусть имеется класс A со статическим методом, который создаёт экземпляр класса:

class A {
  static create() {
   	return new A();
  }
}

Теперь применим к классу наш декоратор:

const decorA = privateConstructor(A);

Если мы попытаемся вызвать конструктор класса сразу, то получим ошибку. Однако, если мы попробуем вызвать сначала вызвать статический метод, а потом конструктор, то всё будет супер:

// Так нельзя
new decorA();
decorA.create();

// А так можно
decorA.create();
new decorA();

Вот это я молодец - расписал очевидную проблему.

Проблема кроется во флаге viaMethod. Нужно сделать так, чтобы он, после вызова метода, обратно возвращался в значение false. А как? Ответ: используем декоратор. Это последний за сегодня декоратор, обещаю.

Сделаем декоратор, который вызывает переданную функцию после того, как метод был вызван и вычислен:

function callFunctionAfterMethod(method, callback) {
  // Да, снова прокси, он тоже последний
	return new Proxy(method, {
    apply: (target, thisArg, args) => {
      // Вызываем метод вместе с this
      const result = Reflect.apply(target, thisArg, args);
      // Вызываем колбэк
      callback();
      
      return result;
    }
  });
}

А теперь поправим немного наш обработчик get:

function privateConstructor(cls) {
  // Флаг, который отвечает за то, что конструктор был вызван через метод,
  // а не через оператор new
  let viaMethod = false;
  
  return new Proxy(cls, {
    // Вешаем обработчик на конструктор, чтобы в случае вызова через new
    // выдавать ошибку
    construct: (target, args) => {
      if (!viaMethod) {
        throw new Error('Cannot use "new" for private constructor');
      }
      
      return Reflect.construct(target, args);
    },
    
    // Вешаем обработчик на каждое свойство и каждый метод
    get: (target, key) => {
      let maybeMethod = Reflect.get(target, key);
      
      // Проверяем, метод это или свойство
      if (maybeMethod instanceof Function) {
        // Переключаем флаг вызова через метод
        viaMethod = true;
        // Декорируем метод так, чтобы после его вызова флаг менялся на false
        maybeMethod = callFunctionAfterMethod(
          maybeMethod.bind(target),
          () => (viaMethod = false)
        );
      }
      
      return maybeMethod;
    }
  });
}

Выводов не будет

Не знаю, что тут можно написать

Теги:
Хабы:
Всего голосов 4: ↑0 и ↓4-4
Комментарии32

Публикации

Истории

Работа

Ближайшие события

28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
2 – 18 декабря
Yandex DataLens Festival 2024
МоскваОнлайн
11 – 13 декабря
Международная конференция по AI/ML «AI Journey»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань