Разбираемся с декораторами в JavaScript

Концепцию декораторов можно рассматривать с разных сторон. Это и шаблон проектирования описанный в известной книге GOF и подход применяемый в функциональном программировании для построения композиции функций. Как правило, когда говорят о декораторах в контексте ECMA Script, то под этим подразумевают функции, которые могут применяться к другим функциям или объектам для изменения или расширения их поведения. Иначе говоря, эти функции декорируют (оборачивают) другие функции или объекты.

Тему декораторов мы разобьём на 2 части. В данной статье попробуем разобраться в сути декораторов, для чего они могут быть полезны, а также как реализованы декораторы в современном JavaScript. Во второй части мы посмотрим на реализацию декораторов в TypeScript.

Декораторы функций

Давайте начнём знакомство с декораторами на примере написания простого декоратора функции, который позволит логировать вызов декорируемой функции. Реализация такого декоратора может выглядеть следующим образом:

В коде выше мы объявили простую функцию sum, которая возвращает сумму 2-х переданных чисел. Далее мы объявили функцию-декоратор logName. Такой декоратор всего лишь возвращает другую функцию в теле которой мы логируем имя функции-аргумента, а затем вызываем переданную функцию и возвращаем результат вызова. Получается, что наш декоратор никак не модифицирует переданную функцию, а только добавляет некое дополнительное поведение.

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

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

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

Здесь мы можем наблюдать ещё одно преимущество декораторов — для каждой задачи мы можем создать отдельный декоратор, который будет делать что-то одно, но делать это хорошо, что позволит создавать комбинации декораторов и применять их к различного вида сущностям (в нашем случае это функция).

Примеры, описанные выше, довольно просты, но позволяют нам понять саму суть декораторов и то как мы можем использовать их для функций. Но что если нам хочется пойти дальше и использовать декораторы, например, для классов?

Современная спецификация декораторов

Для разработки и включения в состав ECMA Script новых фич существует специальный комитет именуемый TC 39 (technical committee 39). Прежде чем попасть в стандарт каждое предложение рассматривается TC 39 и проходит несколько этапов (4 основных), начиная от идеи новой фичи до утверждённой комитетом и включенной в стандарт спецификации. Более подробно про то как TC 39 принимает новые возможности в стандарт можно прочитать в этой статье.

К сожалению, спецификация декораторов всё ещё находится на stage 2 (черновик) и официально не включена в стандарт ECMA Script. Однако мы можем использовать декораторы в своём коде применяя транспиляцию через Babel или TypeScript. В следующем разделе рассматривается как настроить поддержку декораторов используя Babel. Вы можете пропустить этот раздел и ознакомиться с готовой конфигурацией в репозитории, содержащем примеры к этой статье.

Настройка использования декораторов через Babel

Чтобы иметь возможность использовать декораторы в своём JavaScript коде мы должны воспользоваться специальным Babel плагином, реализующем поддержку декораторов. Если мы начинаем настройку с нуля, то для начала нам нужно проинициализировать package.json файл проекта:

Затем необходимо установить нужные зависимости:

Для того чтобы работать с Babel мы поставили 2 основных пакета: @babel/core, который обеспечивает базовую функциональность транспилятора и @babel/cli — для работы с Babel из терминала. Далее мы установили пакет @babel/preset-env, который содержит стандартный набор плагинов для поддержки последних фич JavaScript и которые можно настраивать через один конфиг. Более подробно про пресеты можно узнать из документации к Babel. Следующей командой устанавливаем отдельный плагин для поддержки декораторов — @babel/plugin-proposal-decorators. Ну и напоследок нам нужно установить nodemon для запуска скриптов, которые будут скомпилированы Babel.

После установки зависимостей нам нужно создать файл настроек babel.config.json

В этом файле мы подключаем и конфигурируем установленные ранее пресет и плагин для использования декораторов.

Заключительным этапом будет настройка npm команды для запуска компиляции исходных файлов в режиме отслеживания. Для этого в файле package.json есть специальный блок scripts в котором мы сможем разместить следующую команду:

Этой командой мы указываем Babel компилировать файлы из директории src с выводом в директорию dist. Флаг —watch будет запускать компиляцию при каждом изменении исходных файлов в директории src. На этом основная настройка заканчивается. Чтобы проверить конфигурацию нам нужно создать файл с кодом в директории src (например index.js) после чего запустить команду: npm start. Если всё настроено верно, то после запуска команды будет создана директория dist в которой окажется скомпилированный файл index.js.

Чтобы автоматически каждый раз запускать скомпилированные файлы нам нужно открыть отдельный терминал, перейти в директорию dist и выполнить команду: nodemon filename (опционально). Nodemon запущенный в текущей директории по умолчанию ищет файл с именем index.js. Если файл изменится, то nodemon автоматически перезапустит скрипт.

Использование декораторов классов в JavaScript

Так как же работают декораторы в текущей спецификации? Начнём с того как это реализовано на сегодняшний день в JavaScript с использованием плагина для Babel. Декоратор это по прежнему просто функция, но есть отличия в том как эта функция будет применяться и что в качестве аргументов будет принимать. Давайте более детально посмотрим на простой декоратор класса:

В коде выше мы объявили функцию classDecorator, которая будет являться декоратором нашего класса. В качестве аргумента декоратор класса принимает дескриптор, описывающий объект к которому применяется декоратор. Для нашего класса такой объект имеет два поля: kind — тип объекта и elements — массив элементов класса. Теперь нам нужно применить декоратор. Для этого мы используем специальный символ @ перед его именем. Остановимся подробнее на поле elements объекта-дескриптора. В данном случае в поле будут находиться доступные элементы класса (методы, акцессоры, поля), описанные каждый своим дескриптором. К примеру, если мы добавим в класс метод, то он станет доступен в массиве elements:

Таким образом, применяя декоратор к классу, мы сможем получить доступ к его элементам.

Декораторы методов в JavaScript

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

Здесь мы можем видеть, что в декораторе метода также доступен объект target, но по сравнению с тем объектом, что передавался в декоратор класса, этот объект содержит немного другой набор полей, а именно, теперь отсутствует поле elements, но добавилось два новых поля: placement — содержит значение указывающее расположение декорируемого свойства; descriptor — специальный объект, являющийся дескриптором свойства.

Доступность дескриптора свойства внутри нашего декоратора позволяет нам изменять поведение или характеристики свойства. Например, давайте напишем декоратор для запрета переопределения метода в прототипе (что для JavaScript вполне легально):

Подробнее про дескриптор свойства

Данный дескриптор описывает различные параметры свойства объекта такие как: value — текущее значение (в нашем случае это функция); writable — возможность перезаписывать свойство (например, назначать другую функцию); enumerable — доступность свойства при итерации по объекту; configurable — возможность переопределять непосредственно сам дескриптор данного свойства. При этом вышеописанные параметры относятся к т.н. дескриптору данных, который не описывает поведение при обращении или изменении свойства. Чтобы описать такое поведение используется дескриптор доступа, который содержит два поля: get — для указания функции, которая будет вызываться при чтении свойства и set — для указания функции, которая будет вызываться при изменении свойства. Ещё один важный момент — дескриптор свойства может относиться только к одному из указанных выше типов. При описании дескриптор должен описывать либо только данные, либо только поведение при доступе к этим данным (указание get и set методов). Более подробно ознакомиться с дескриптором свойства, а также тем как его определять, можно в документации на MDN.

Декораторы полей в JavaScript

Аналогично методу, декоратор можно применить также и для полей класса или акцессоров (get/set). Давайте используем уже существующий декоратор @readonly для одного из полей класса:

В примере выше мы объявили класс со свойством firstname, которое уже проинициализировано значением и для этого свойства применён декоратор readonly. Использование данного декоратора, также как и в случае с методом, запретит присваивать другое значение этому свойству.

На текущий момент в актуальной версии черновика ECMA Script поддерживаются следующие декораторы: декораторы классов, методов, полей и акцессоров. Существует также отдельный документ, который описывает так называемые EXTENSIONS, которые содержат к примеру: альтернативный синтаксис аннотации декораторов, декораторы параметров, let и const декораторы, а также другие различные расширения использования декораторов. Как и основное предложение этот документ находится в статусе черновика, поэтому нет уверенности в том, что вообще что-либо описанное в этом документе попадёт в стандарт.

Заключение

На этом мы завершим обзор того как устроены декораторы в JavaScript на сегодняшний день. В следующей части мы перейдём к рассмотрению устройства декораторов в TypeScript и попробуем понять в чём основная разница между этими двумя реализациями. Надеюсь, что к моменту выхода статьи спецификация ещё не устареет =)
Спасибо за внимание!

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