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