Создание типов Enum в Javascript

При создании расширенных типов Enum в Typescript мы использовали удобные модификаторы доступа только для чтения и частного доступа в Typescript для создания шаблона перечисления со значительной гибкостью и расширенными функциями. Следующим логическим шагом будет формирование параллельного шаблона для перечислений в Javascript - для случаев, когда среда Javascript является предпочтительной или необходимой.

Уловка здесь в том, что Javascript, а точнее Javascript ES5 и ES6 - самые популярные целевые версии - не поддерживают свойства только для чтения, и определенно не для частных аксессоров. Чтобы реализовать здесь шаблон перечисления, нам нужно будет использовать более глубокие, возможно, не обычные инструменты или шаблоны. И это не так уж плохо! Можно многому научиться, просто играя с ними, что мы и будем делать в ближайшее время. Чтобы быть более точным, мы будем использовать несколько менее частых методов класса Object, а также в ES6 Proxy API.

Зачем использовать перечисления в Javascript?

Если вы не уверены, что такое перечисления или зачем их использовать (особенно на языке, который не поддерживает их изначально, например, Javascript) - мы обсудили перечисления в целом, а также когда они могут быть полезны, в предыдущей статье.

Короче говоря, перечисления - это особый тип «закрытого» класса, все экземпляры которого четко определены во время компиляции и доступны через сам класс (обычно как статические переменные, например Enum.INSTANCE). Они полезны для моделирования типа, экземпляры которого являются предопределенными и (обычно) постоянными, что обеспечивает лучшее соблюдение типов и удобочитаемость.

Например, свойства CSS, такие как «положение» или «отображение», постоянные наборы объектов, такие как планеты в солнечной системе, и многие лингвистические концепции (например, времена) могут быть правильно смоделированы с использованием типов перечислений, и список идет на; действительно, многие приложения имеют постоянные объекты или сущности, которые можно обновлять с помощью перечислений.

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

Начнем с самой простой, самой простой реализации:

Простейшая и понятная реализация

Предположим, вам нужно простое постоянное отображение значения ключа (например, для поиска или индексации); В качестве примера рассмотрим карту между кодами ошибок HTTP-ответа и ориентировочными сообщениями, которые приложение отображает, когда они возвращаются:

Очевидно, что это упрощенный пример, но можно получить соответствующее сообщение для некоторого кода состояния, возвращаемого HTTP-запросом, и отобразить его пользователю. Однако обычно вы хотите, чтобы пары статус-сообщение оставались неизменными - вы не хотите, чтобы кто-то (другой программист, работающий над проектом, или вы), например, изменил сообщение 200 на «Большая плохая ошибка!». И вы обычно не хотите, чтобы кто-то добавлял новые сообщения или удалял существующие, как для статуса -1, поскольку коды статуса постоянны.

Именно это и делает метод Javascript: Object.freeze (). Согласно MDN:

Метод Object.freeze () замораживает объект. Замороженный объект больше нельзя изменить; замораживание объекта предотвращает добавление к нему новых свойств, удаление существующих свойств […] и предотвращает изменение значений существующих свойств. Кроме того, замораживание объекта также предотвращает изменение его прототипа. freeze () возвращает тот же объект, который был передан.

Заморозить наши коды состояния очень просто: нам просто нужно передать их функции:

Object.freeze(status_codes);

Функция замораживает аргумент и возвращает его; в идеале мы должны объявить этот объект в специальном файле и экспортировать его:

export default Object.freeze(status_codes);

Вот и все! у вас есть простое перечисление - постоянное статическое отображение кодов состояния в сообщения. Перечисления - и вообще замороженные объекты - можно использовать во многих местах для улучшения кода и предотвращения ошибок, которые легко не заметить, особенно при работе в группах. Рекомендуется замораживать объекты, которые должны быть статичными. Однако обратите внимание, что Object.freeze () не застывает глубоко - если у вас есть вложенные объекты, рассмотрите возможность вызова Object.freeze () и для них.

Эти виды перечислений параллельны встроенной реализации перечисления в Typescript. Однако, как мы видели в предыдущем обсуждении, если мы будем рассматривать перечисления как особый тип классов, мы можем значительно продвинуться с ними; К счастью, мы можем сделать это и в Javascript, используя несколько приемов.

Мы рассмотрим две продвинутые реализации перечисления в Javascript: первая работает на ES5 (и, конечно, более поздних версиях) и использует систему прототипов Javascript. Второй использует Proxy API, представленный в ES6.

Реализация в ES5

Прежде всего, поскольку поддержка ES6 сегодня широко распространена, либо напрямую, либо через polyfills / babel, ценность реализации ES5, на мой взгляд, в основном образовательная.

С помощью Object.freeze () (или аналогичного метода, как мы скоро увидим) мы можем предотвратить изменение, добавление или удаление свойств объекта - или, более конкретно, экземпляра класса. Следовательно, основная проблема, с которой мы сталкиваемся, - это сокрытие конструктора класса (делая его закрытым с точки зрения ООП), чтобы новые экземпляры не могли создаваться динамически. Однако эта проблема оказывается более сложной, чем может показаться, поскольку на самом деле нам действительно нужен конструктор для создания наших экземпляров и только потом каким-то образом заставить его исчезнуть.

Для решения этой проблемы используются классы JS, являющиеся просто функциями определенного типа (конструкторы): мы создаем класс (функцию), например. Enum с конструктором never - попытка создать экземпляр Enum вызывает ошибку. Затем мы создаем другой класс - EnumInner, который наследует Enum, но имеет соответствующий конструктор; Затем мы используем конструктор для создания экземпляра EnumInner (который, в свою очередь, также является экземпляром Enum), а также желаемых статических свойств и методов, все из которых мы привязываем к классу Enum. Наконец, мы предоставляем класс Enum, а не класс EnumInner, тем самым предотвращая создание новых экземпляров извне.

Очень удобно делать все вышеперечисленное внутри функции, в частности анонимной функции, которую мы вызываем сразу после ее определения. Таким образом, используя способ работы переменных и функций с ограниченной областью видимости в Javascript, мы можем предоставить доступ только к общедоступному конструктору (Enum). На самом деле это обычный шаблон Javascript; Например, Typescript использует его при переносе пространств имен в Javascript. Опять же, также рекомендуется объявить перечисление в отдельном файле и экспортировать его.

Давайте реализуем тип Todo Status из предыдущей статьи, используя этот шаблон:

Сначала мы определяем основной макет класса:

Обратите внимание, что мы определяем анонимную функцию и немедленно вызываем ее - это позволяет нам определить класс EnumInner без доступа к нему извне.

Затем давайте определим общедоступный и внутренний конструкторы и заставим внутренний класс расширить внешний класс, установив его прототип:

Метод toJSON () обеспечивает легкую сериализацию объектов Status.

Давайте теперь рассмотрим добавление самих экземпляров и статических методов, включая геттер .values ​​для получения всех экземпляров перечисления. Добавление их должно быть довольно простым - мы можем присвоить им статус, который позже будет показан, и они будут отображаться вместе с ним. Однако нам нужно предотвратить их изменение извне, что-то вроде модификатора readonly в Typescript. И мы не можем просто заморозить их - ссылки на экземпляры все еще могут быть удалены или полностью переназначены извне класса (например, вызов Status.COMPLETED = null).

К счастью, у класса Object и здесь есть наши спины: для такого рода целей существует метод Object.defineProperty (). Согласно MDN:

Этот метод позволяет точное добавить или изменить свойство объекта. […] По умолчанию значения, добавленные с помощью Object.defineProperty (), неизменяемы и не перечисляются.

По сути, объекты Javascript (как правило) имеют дополнительные «скрытые» флаги, которые определяют их поведение, в частности, при попытке изменить, удалить или перечислить их. Object.defineProperty () позволяет нам настраивать эти флаги. Мы будем использовать аналог этой функции, Object.defineProperties (), который позволяет определять более одного такого свойства за раз.

Если вы не знакомы с этими функциями, ознакомьтесь с Object.defineProperty () и Object.defineProperties (). Они необходимы для понимания Javascript за кулисами и могут многому вас научить.

Возвращаясь к нашему статусному классу, использовать метод Object.defineProperties () довольно просто. мы можем определить конструкцию всех необходимых нам экземпляров, используя конструктор StatusInner:

Обратите внимание, что экземпляры помечены как enumerable: это означает, что они могут быть повторены, например, с помощью Object.keys () или цикла for… of. По умолчанию в Object.defineProperties () установлен флаг перечисления false, поэтому нам нужно передать его явно.

А теперь давайте также определим несколько статических методов и геттер .values; они не должны быть перечисляемыми:

Обратите внимание на специальный синтаксис свойства values: у него есть поле get, которое определяет геттер (в основном, геттер позволяет нам получать доступ к таким значениям, как Status.values ​​вместо Status.values ​​()). Функция fromString () возвращает статус по имени или выдает ошибку, если нет статуса с соответствующим именем (как в предыдущей статье).

Наконец, нам нужно заморозить публичный класс Status и вернуть (раскрыть) его:

Готово! Давайте посмотрим на полный пример:

Этот полный пример, конечно, можно легко обобщить в общий класс Enum и даже во фрагмент. Общий класс Enum, следующий за этой реализацией, можно найти здесь.

Реализация в ES6 с использованием Proxy API

Что ж, решение ES5 работает, но оно определенно многословно (простая реализация, как видно из полного примера выше, занимает 100 строк кода!), И довольно сложно. В ES6, среди многих других полезных инструментов, мы получили Proxy API, который позволит нам представить альтернативное, краткое и более интуитивное решение:

Как следует из названия, Proxy API (доступный с помощью Proxy class) контролирует весь доступ к объекту. Многие прогрессивные платформы, такие как Vue.js, широко используют Proxy API. Мы можем использовать его, обернув определенный объект, функцию или класс в конструктор Proxy:

new Proxy(*target, handler*)

target, конечно же, это объект, для которого вы хотите создать прокси; обработчик - это объект, содержащий ловушки - обработчики различных операций, которые вы можете выполнять с объектом, таких как получение и установка полей, удаление полей и, что наиболее важно для нас, создание новых экземпляров. Страница MDN на прокси описывает все различные ловушки-обработчики, которые вы можете использовать; Я рекомендую вам проверить это.

Нас интересуют следующие ловушки: construct (), defineProperty (), deleteProperty (), set () и setPrototypeOf (); при попытке выполнить каждую из этих операций с классом перечисления мы хотим выбросить TypeError со связанным сообщением. Обратите внимание, что эти ошибки не являются ошибками времени компиляции (к сожалению, Javascript не имеет возможности проверить, что эти операции не вызываются во время компиляции), но они по-прежнему препятствуют изменению класса извне.

Также обратите внимание, что использование Proxy обертывает наш класс, и доступ к исходному классу Enum все еще может быть предоставлен. Таким образом, тем более рекомендуется объявить класс Enum в отдельном файле и экспортировать только прокси, желательно как экспорт по умолчанию.

Приступим к делу - на этот раз мы сделаем это в общих чертах с самого начала. Во-первых, давайте определим стандартный класс Enum:

Самое главное, обратите внимание на строку 19 (Object.freeze (this)). Это предотвращает «глубокие» изменения класса Enum, т.е. изменения свойств его экземпляров, путем замораживания экземпляра после построения.

Затем давайте определим прокси и, наконец, экспортируем его:

Enum.name »- это имя класса (в данном случае Enum ).

И полный пример:

Нам удалось сократить наш код вдвое! И теперь он стал более читаемым и понятным. Это тоже работает как шарм:

Заключение

В этой статье мы обсудили перечисления и шаблон перечисления в Javascript. Мы рассмотрели, почему и когда они могут быть важны, и три способа их реализации - простой метод, метод в ES5 и метод ES6 с использованием прокси. Помимо значений перечислений, мы изучили некоторые методы Object, которые могут быть действительно полезны (freeze (), defineProperty () и getPrototypeOf () / setPrototypeOf ()), а также основы Proxy API (и полезный шаблон для сложных классов ES5) и многое другое, что происходит за кулисами в Javascript.

Если вы этого не сделали, ознакомьтесь с предыдущей статьей Создание расширенных типов Enum в Typescript, где мы прошли аналогичный процесс в Typescript; реализация была более интуитивной и простой, так как Typescript уже имеет базовые возможности ООП.

Надеюсь, вам понравилась эта статья, и вы кое-чему из нее научились. Я хотел бы услышать ваши мысли и отзывы.