Введение
Месяц назад вышел новый стандарт 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; } }); }
Выводов не будет
Не знаю, что тут можно написать

