JavaScript декораторы наконец-то в Stage 3

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!

18 апреля 2022 года, после 5 лет доработки (первый коммит от 30 апреля 2017 года), proposal по декораторам наконец-то достиг 3 стадии, что означает что у него есть спецификация, тестовая имплементация и осталась только полировка на основе фидбека от разработчиков. Учитывая что это уже четвертая (!) итерация декораторов, их переход в стадию принятия это эпохальное событие для JS - не припомню ни одной другой фичи, которая прошла такой длинный и тернистый путь, с диаметрально разными подходами и аж двумя разными legacy-имплементациями, в Babel и TypeScript. Давайте же посмотрим на неё повнимательней.

Ссылки

https://github.com/tc39/proposal-decorators - репозиторий самого предложения, включая все предыдущие версии (в истории коммитов).

http://senocular.github.io/articles/js_history_of_decorators.html - история предложений, включая ссылки на все четыре основные версии.

https://javascriptdecorators.org/ - независимая имплементация

https://babeljs.io/docs/en/babel-plugin-proposal-decorators - плагин для Babel

Кстати, новая версия датируется в Babel как 2021-12 - потому что была представлена на саммите TC39 в декабре 2021 года.

Чем отличается от предыдущих версий

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

Во-вторых, главное отличие новых декораторов: они работают только с сущностью которую декорируют (класс, поле класса, метод, геттер/сеттер и аксессор - новая сущность, о которой далее), а не с дескрипторами свойств и/или прототипами классов, как легаси подходы.

То есть они не способны добавить новые сущности в прототип/инстанс класса или хотя бы изменить их вид (с поля на геттер/сеттер, например), а могут только преобразовать ту сущность, которая описана в исходном коде - обернуть её в дополнительную логику или полностью заменить на другую, но того же вида.

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

Демо и синтаксис применения декораторов

Ну и сразу полный пример со всеми возможными комбинациями синтаксиса:

//export должен быть перед декоратором
export default
//декоратор класса, может изменять сам класс
@defineElement("some-element")
class SomeElement extends HTMLElement {
  //декоратор поля - может заменить значение поля при инициализации класса
  //все дальнейшие чтения/записи он не отслеживает
  @inject('some-dep')
  dep

  //новый синтаксис - аксессор
  //по факту просто сахар для пары геттер/сеттер
  //похож на автоматически реализуемые свойства в C#
  //могут быть и приватными и статическими
  //декоратор может отслеживать чтение/запись
  @reactive accessor clicked = false

  //ну с методами и прочим все как обычно
  @logged
  someMethod() {
    return 42
  }

  //да, с приватными элементами тоже работает, как и со статическими
  //название декоратора может быть через точку
  @random.int(0, 42)
  #val

  @logged
  get val() {
    return this.#val
  }

  @logged
  set val(value) {
    this.#val = value
  }

  //апофеоз:
  //статический приватный аксессор c декоратором со сложным доступом
  @(someArr[3].someFunc('param'))
  static acсessor #name = 'some-element'
}

Текущие имплементации полифиллов этот пример полностью переварить еще не могут но, думаю, в скором времени это исправится.

Синтаксис для применения декораторов в целом не слишком отличается от привычного, есть только пара деталей:

  1. Декоратор класса должен идти после export (если он есть) - это наверное главное отличие от статус-кво.

  2. Для "обычного" применения декоратора можно использовать идентификатор, точку и вызов функции - @dotted.form.with('some-call')

  3. Для "сложного" применения можно использовать синтаксис со скобками: @(complex[1])

Написание декораторов

Тут никаких особых сюрпризов - декоратор это обычная функция с таким типом:

context предоставляет, как ни странно, контекст, сведения о месте применения декоратора, где:

  • kind - вид элемента, на который применяется декоратор

  • name - название элемента

  • access - объект который позволяет в произвольный момент времени получить/установить значение элемента, может пригодиться, например, для DI. Разрешен только для элементов класса, но не для самих классов (то есть get или set есть только когда kind != 'class')

  • private и static - есть ли у элемента класса соответствующие модификаторы

  • addInitializer позволяет выполнить код после того как сам класс (не инстанс!) или элемент класса полностью определен - например, в нем можно зарегистрировать класс в DI или забиндить метод. Не применим только для поля класса (то есть определен когда kind != 'field' - об этом далее)

Input и Output зависят от kind, но в целом Input - это значение элемента как оно написано в коде, а Output - значение на которое оно будет заменено в рантайме.

Важный нюанс - для полей класса (когда kind == 'field') Input всегда undefined, а Output может быть функцией вида (initValue: unknown) => any - эта функция вызывается при инициализации класса для вычисления начального значения поля. Именно из-за этого для поля класса не передается addInitializer - Output его заменяет.

Пример декоратора logged:

function logged(value, { kind, name }) {
  if (kind === "method") {
    return function (...args) {
      console.log(`starting ${name} with arguments ${args.join(", ")}`);
      const ret = value.call(this, ...args);
      console.log(`ending ${name}`);
      return ret;
    };
  }
  if (kind === "field") {
    return function (initialValue) {
      console.log(`initializing ${name} with value ${initialValue}`);
      return initialValue;
    };
  }
  if (kind === "class") {
    return class extends value {
      constructor(...args) {
        super(...args);
        console.log(`constructing an instance of ${name} with arguments ${args.join(", ")}`);
      }
    }
}

Ну или вот customElement с использованием addInitializer:

function customElement(name) {
  (value, { addInitializer }) => {
    addInitializer(function() {
      customElements.define(name, this);
    });
  }
}

@customElement('my-element')
class MyElement extends HTMLElement {
  static get observedAttributes() {
    return ['some', 'attrs'];
  }
}

Больше примеров (в том числе и с применением access для DI) смотрите на гитхабе.

Аксессоры

Ограничения новых декораторов в виде запрета на изменение вида элемента в целом логичны, но они убивают один крайне важный юзкейс декораторов - когда поле превращается в пару геттер/сеттер с дополнительной логикой вокруг. Это может быть, например, логгирование изменений поля для отладки, а может быть полноценная система реактивности, как в MobX, который, по сути, основан на этом хаке:

import {computed, observable, autorun} from 'mobx'

class Counter {
  	//вот здесь поле превращается в геттер/сеттер
    @observable num = 1
  	//а будет так
    @observable accessor num = 1
    
    @computed
    get double() {
        return this.num * 2
    }
}

const counter = new Counter()

//выведет 2
autorun(() => console.log(counter.double)) 

//когда изменяем num, изменится и double
counter.num = 2
//autorun выполняется снова и выводит 4

С новыми декораторами все такие поля придется помечать как accessor что, конечно, не слишком весело, но в целом терпимо и может отслеживаться, например, тайпскриптом. Под капотом работать это будет примерно так:

class C {
  accessor x = 1;
}

//Раскрывается в...

class C {
  #x = 1;

  get x() {
    return this.#x;
  }

  set x(val) {
    this.#x = val;
  }
}

Имплементации

Пока ждем реализации в основных тулзах - в первую очередь это, конечно, поддержка аксессоров как нового синтаксиса. Когда IDE, TypeScript и Babel (esbuild и т.д.) смогут их корректно обрабатывать, сделать полифиллы будет не так и сложно.

И я крайне надеюсь что TypeScript будет корректно обрабатывать типы декораторов при замене значений - сейчас декоратор никак не может повлиять на тип декорируемого значения.

Ссылки для отслеживания внедрения:

https://github.com/microsoft/TypeScript/issues/48885 - TypeScript - фича включена в планы на версию 4.8.

https://github.com/evanw/esbuild/issues/104 - esbuild - ждут реализации в TS/node/браузерах.

Ну а потом последует волна переезда на новую реализацию со стороны экосистемы. К счастью, декораторы в JS не так и распространены, и при этом новые декораторы могут быть реализованы в библиотеках вместе со старыми - их сигнатура отличается от Babel/TS декораторов.

Дождались, в общем.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Пользуетесь декораторами в JS?
25% Да! 2
75% Нет… 6
Проголосовали 8 пользователей. Воздержался 1 пользователь.
Источник: https://habr.com/ru/post/666688/


Интересные статьи

Интересные статьи

Деструктурирующее присваивание в JavaScript — это изящный способ извлечения значений из массивов и объектов, при котором в полной мере реализуется принцип DRY (англ. Don’t repeat yourself — ...
Украинский профильный ресурс DOU.UA провел очередной ежегодный опрос о языках программирования, в рамках которого было собрано 7211 анкет (92% респондентов находятся в Ук...
Всем привет. Статья о делегирование событий в JavaScript и реализация его в react.js. О чем собственно речь? Зачем и почему? Для начала давайте кратко обсудим: что есть событие; как проис...
Производительность — это один из важнейших вопросов, встающих перед разработчиками веб-страниц или веб-приложений. Никто не будет рад «падающему» от чрезмерной нагрузки приложению, или странице, ...
Тема статьи навеяна результатами наблюдений за методикой создания шаблонов различными разработчиками, чьи проекты попадали мне на поддержку. Порой разобраться в, казалось бы, такой простой сущности ка...