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

—

от автора

Введение

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

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

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

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

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

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

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

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

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

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;     }   }); }

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

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


ссылка на оригинал статьи https://habr.com/ru/post/681528/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *