[Разработка веб-сайтов, JavaScript, Программирование] Будущее JavaScript: декораторы

Автор Сообщение
news_bot ®

Стаж: 6 лет 3 месяца
Сообщений: 27286

Создавать темы news_bot ® написал(а)
13-Янв-2021 13:33


Доброго времени суток, друзья!
Представляю вашему вниманию адаптированный перевод нового варианта предложения (сентябрь 2020 г.), касающегося использования декораторов в JavaScript, с небольшими пояснениями относительно характера происходящего.
Впервые данное предложение прозвучало около 5 лет назад и с тех пор претерпело несколько значительных изменений. В настоящее время оно (по-прежнему) находится на второй стадии рассмотрения.
Если вы раньше не слышали о декораторах или хотите освежить свои знания, рекомендую ознакомиться со следующими статьями:

Итак, что такое декоратор? Декоратор (decorator) — это функция, вызываемая на элементе класса (поле или методе) или на самом классе в процессе его определения, оборачивающая или заменяющая элемент (или класс) новым значением (возвращаемым декоратором).
Декорированное поле класса обрабатывается как обертка из геттера/сеттера, позволяющая извлекать/присваивать (изменять) значение этому полю.
Декораторы также могут аннотировать элемент класса метаданными (metadata). Метаданные — это коллекция простых свойств объекта, добавленных декораторами. Они доступны как набор вложенных объектов в свойстве [Symbol.metadata].
Синтаксис
Синтаксис декораторов, помимо префикса @ (@decoratorName), предполагает следующее:
  • Выражения декораторов ограничены цепочкой переменных (можно использовать несколько декораторов), доступом к свойству с помощью ., но не c помощью [], и вызовом посредством ()
  • Декорироваться могут не только определения классов, но и их элементы (поля и методы)
  • Декораторы классов указываются после export и default

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

1. Вычисление декораторов
Декораторы оцениваются как выражения вместе с вычисляемыми именами свойств. Это происходит слева направо и сверху вниз. Результат декоратора сохраняется в своего рода локальную переменную, которая вызывается (используется) после завершения определения класса.
2. Вызов декораторов
Декоратор вызывается с двумя аргументами: оборачиваемым элементом и, опционально, объектом контекста.
Оборачиваемый элемент: первый параметр
Первый аргумент, который оборачивается декоратором, это то, что мы декорируем (извиняюсь за тавтологию):
  • Если речь идет о простом методе, методе инициализации, геттере или сеттере: соответствующая функция
  • Если о классе: сам класс
  • Если о поле: объект с двумя свойствами:
    • get: функция без параметров, которая вызывается с получателем (receiver), представляющим собой объект, возвращающий содержащееся в нем значение
    • set: функция, принимающая один параметр (новое значение), которая вызывается с получателем, представляющим собой переданный объект, и возвращает undefined

Объект контекста: второй параметр
Объект контекста — объект, передаваемый декоратору в качестве второго аргумента — содержит следующие свойства:
  • kind: имеет одно из следующих значений:
    • «class»
    • «method»
    • «init-method»
    • «getter»
    • «setter»
    • «field»
  • name:
    • публичное поле или метод: name — строковый или символьный ключ свойства
    • частное поле или метод: отсутствует
    • класс: отсутствует
  • isStatic:
    • статическое поле или метод: true
    • поле или метод экземпляра: false
    • класс: отсутствует

«Target» (конструктор или прототип) не передается декораторам полей или методов по той причине, что она («цель») еще не сконструирована в момент вызова декоратора.
Возвращаемое значение
Возвращаемое значение зависит от типа декоратора:
  • класс: новый класс
  • метод, геттер или сеттер: новая функция
  • поле: объект с тремя свойствами:
    • get
    • set
    • initialize: функция, вызываемая с тем же аргументом, что и set, возвращающая значение, которое используется для инициализации переменной. Данная функция вызывается, когда настройка низлежащего (внутреннего) хранилища (underlying storage) зависит от инициализатора поля или определения метода
  • метод init: объект с двумя свойствами:
    • method: функция, заменяющая метод
    • initialize: функция без аргументов, возвращаемое значение которой игнорируется, и которая вызывается с вновь созданным объектом в качестве получателя

3. Применение декораторов
Декораторы применяются после их вызова. Промежуточные этапы алгоритма работы декораторов не могут быть зафиксированы — вновь созданный класс является недоступным до тех пор, пока не будут применены все декораторы методов и полей экземпляров.
Декораторы классов вызываются после применения декораторов полей и методов.
Наконец, применяются декораторы статических полей.
Семантика декраторов полей
Декоратор поля класса представляет собой пару геттер/сеттер — упаковку для частного поля. Поэтому код:
function id(v) { return v }
class C {
  @id x = y
}

имеет такую семантику:
class C {
  // префикс # указывает на приватность переменной-поля
  #x = y
  get x() { return this.#x }
  set x(v) { this.#x = v }
}

Декораторы полей ведут себя подобно частным полям. Следующий код выбросит исключение TypeError из-за того, что мы пытаемся получить доступ к «y» до ее добавления в экземпляр:
class C {
  @id x = this.y
  @id y
}
new C // TypeError

Пара геттер/сеттер — это обычные методы объекта, которые являются неперечислимыми (неперечисляемыми, если угодно), как и другие методы. Содержащиеся в ней приватные поля добавляются один за другим, вместе с инициализаторами, как обычные частные поля.
Цели проектирования
  • Должно быть одинаково легко как использовать встроенные декораторы, так и писать собственные
  • Декораторы должны применяться только к декорируемым объектам без побочных эффектов

Случаи применения
  • Хранение метаданных в классах и методах
  • Преобразование поля в аксессор
  • Оборачивание метода или класса (такое использование декораторов чем-то напоминает проксирование объектов)

Примеры
Примеры реализации и использования декораторов.
@logged
Декоратор @logged выводит в консоль сообщения о начале и завершении выполнения метода. Существуют другие популярные декораторы, оборачивающие функции, например: @deprecated, debounce, @memoize и т.д.
Использование:
// расширение .mjs указывает на файл-модуль
import { logged } from './logged.mjs'
class C {
  @logged
  m(arg) {
    this.#x = arg
  }
  @logged
  set #x(value) { }
}
new C().m(1)
// запуск m с аргументами 1
// запуск set #x с аргументами 1
// завершение set #x
// завершение m

@logged может быть реализован в JavaScript в качестве декоратора. Декоратор — это функция, которая вызывается с аргументом, содержащим декорируемый элемент. Таким элементом может быть метод, геттер или сеттер. Декораторы могут вызываться со вторым аргументом — контекстом, однако, в данном случае он нам не нужен.
Значение, возвращаемое декоратором, заменяет оборачиваемый элемент. Для методов, геттеров и сеттеров, возвращаемое значение — это заменяющая их функция.
// logged.mjs
export function logged(f) {
  // получаем название функции
  const name = f.name
  function wrapped(...args) {
    // сообщаем о запуске функции
    console.log(`запуск ${name} с аргументами ${args.join(', ')}`)
    // получаем результат выполнения функции
    const ret = f.call(this, ...args)
    // сообщаем о завершении функции
    console.log(`завершение ${name}`)
    // возвращаем результат
    return ret
  }
  // Object.defineProperty() определяет новое или изменяет существующее свойство объекта
  // https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty
  Object.defineProperty(wrapped, 'name', { value: name, configurable: true })
  // возвращаем обертку
  return wrapped
}

Результат транспиляции приведенного примера может выглядеть следующим образом:
let x_setter
class C {
  m(arg) {
    this.#x = arg
  }
  static #x_setter(value) { }
  // предложение - статические блоки инициализации класса (class static initialization blocks)
  // https://github.com/tc39/proposal-class-static-block
  static { x_setter = C.#x_setter }
  set #x(value) { return x_setter.call(this, value) }
}
C.prototype.m = logged(C.prototype.m, { kind: "method", name: "m", isStatic: false })
x_setter = logged(x_setter, {kind: "setter", isStatic: false})

Обратите внимание, что геттеры и сеттеры декорируются раздельно. Аксессоры (вычисляемые свойства) не объединяются, как в предыдущих предложениях.
@defineElement
HTML Custom Elements (пользовательские элементы, часть веб-компонентов) позволяют создавать свои собственные HTML-элементы. Регистрация элементов осуществляется с помощью customElements.define. Вот как можно выполнить регистрацию элемента с помощью декораторов:
import { defineElement } from './defineElement.js'
@defineElement('my-class')
class MyClass extends HTMLElement { }

Классы могут декорироваться наравне с методами и аксессорами.
// defineElement.mjs
export function defineElement(name, options) {
  return klass => {
    customElements.define(name, klass, options); return klass
  }
}

Декоратор принимает аргументы, которые сам же использует, поэтому он реализуется как функция, возвращающая другую функцию. Вы можете думать об этом как о «фабрике декораторов»: после передачи аргументов, вы получаете другой декоратор.
Декораторы, добавляющие метаданные
Декораторы могут снабжать элементы класса метаданными путем добавления свойства metadata к передаваемому им объекту-контексту. Все объекты, содержащие метаданные, объединяются с помощью Object.assign и помещаются в свойство класса [Symbol.metadata]. Например:
// добавление метаданных к классу
@annotate({x: 'y'}) @annotate({v: 'w'}) class C {
  // добавление метаданных к методу
  @annotate({a: 'b'}) method() { }
  // добавление метаданных к полю
  @annotate({c: 'd'}) field
}
C[Symbol.metadata].class.x                    // 'y'
C[Symbol.metadata].class.v                    // 'w'
// методы, предоставляемые классом, являются распределенными или совместными,
C[Symbol.metadata].prototype.methods.method.a // 'b'
// а поля собственными
C[Symbol.metadata].instance.fields.field.c    // 'd'

Обратите внимание, что формат представления объекта с аннотацией является примерным и впоследствии может быть уточнен. Основная задача примера — показать, что аннотация — это всего лишь объект, не требующий использования библиотек для чтения или записи данных в него, он создается системой автоматически.
Рассматриваемый декоратор может быть реализован так:
function annotate(metadata) {
  return (_, context) => {
    context.metadata = metadata
    return _
  }
}

При каждом вызове декоратора ему передается новый контекст, затем свойство metadata, при условии, что оно не равняется undefined, включается в [Symbol.metadata].
Обратите внимание, что метаданные, добавляемые к самому классу, а не к его методу, недоступны для декораторов, объявленных в классе. Добавление метаданных в класс происходит в конструкторе после вызова всех «внутренних» декораторов во избежание потери данных.
@tracked
Декоратор @tracked наблюдает за полем класса и вызывает метод render при вызове сеттера. Данный паттерн и похожие на него паттерны широко используются различными фреймворками для решения проблемы повторного рендеринга.
Семантика декорирумых полей предполагает обертку из геттера/сеттера вокруг некоторого приватного хранилища данных. @tracked может обернуть пару геттер/сеттер для реализации логики повторного рендеринга:
import {tracked} from './tracked.mjs'
class Element {
  @tracked counter = 0
  increment() { this.counter++ }
  render() { console.log(counter) }
}
const e = new Element()
e.increment() // в консоль выводится 1
e.increment() // 2

При декорировании поля, «обернутое» значение представляет собой объект с двумя свойствами: функциями get и set, предназначенными для управления внутренним хранилищем. Они сконструированы таким образом, чтобы автоматически привязываться к экземпляру (с помощью call()).
// tracked.mjs
export function tracked({ get, set }) {
  return {
    get,
    set(value) {
      if (get.call(this) !== value) {
        set.call(this, value)
        this.render()
      }
    }
  }
}

Ограниченный доступ к приватным полям и методам
Порой некоторому коду, находящемуся за пределами класса, может потребоваться доступ к приватным полям или методам. Например, для обеспечения взаимодействия между двумя классами или в целях тестирования кода внутри класса.
Декораторы делают возможным доступ к приватным полям и методам. Эта логика может быть инкапсулирована в объекте с приватными ключами-ссылками, предоставляемыми по необходимости.
import { PrivateKey } from './private-key.mjs'
let key = new PrivateKey()
export class Box {
  @key.show #contents
}
export function setBox(box, contents) {
  return key.set(box, contents)
}
export function getBox(box) {
  return key.get(box)
}

Обратите внимание, что приведенный пример — это своего рода хак, который легче реализовать с помощью конструкций наподобие ссылок на приватные имена с помощью private.name или расширения области видимости приватных имен с помощью private/with. Однако он показывает, как данное предложение органично расширяет существующий функционал.
// private-key.mjs
export class PrivateKey {
#get
#set
show({ get, set }) {
  assert(this.#get === undefined && this.#set === undefined)
  this.#get = get
  this.#set = set
  return { get, set }
}
get(obj) {
  return this.#get.call(obj)
}
set(obj, value) {
  return this.#set.call(obj, value)
}
}

@deprecated
Декоратор @deprecated выводит в консоль предупреждение об использовании устаревших полей, методов или аксессоров. Пример использования:
import { deprecated } from './deprecated.mjs'
export class MyClass {
  @deprecated field
  @deprecated method() { }
  otherMethod() { }
}

Для того, чтобы обеспечить возможность работы декоратора с разными элементами класса, поле kind контекста сообщает декоратору тип синтаксической конструкции, признаваемой устаревшей. Данная техника также позволяет выбрасывать исключения, когда декоратор используется в недопустимом контексте, например: внутренний класс не может быть помечен как устаревший, поскольку доступ к нему запретить невозможно.
function wrapDeprecated(fn) {
  let name = fn.name
  function method(...args) {
    console.warn(`код ${name} признан устаревшим`)
    return fn.call(this, ...args)
  }
  Object.defineProperty(method, 'name', { value: name, configurable: true })
  return method
}
export function deprecated(element, { kind }) {
  switch (kind) {
    case 'method':
    case 'getter':
    case 'setter':
      return wrapDeprecated(element)
    case 'field': {
      let { get, set } = element
      return { get: wrapDeprecated(get), set: wrapDeprecated(set) }
    }
    default:
      // включая 'class'
      throw new Error(`${kind} является недопустимой целью @deprecated`)
  }
}

Декораторы методов, требующие предварительной настройки
Некоторые декораторы методов основаны на выполнении кода при создании экземпляра класса. Например:
  • Декоратор @on('event') для методов класса расширяет HTMLElement, который регистрирует этот метод как обработчик события в конструкторе
  • Декоратор @bound является эквивалентом this.method = this.method.bind(this) в конструкторе

Существуют разные способы использования названных декораторов.
Вариант 1: конструкторы и метаданные
Эти декораторы представляют собой комбинацию метаданных и примеси (mixin), содержащей операции по инициализации, которые используются в конструкторе.
@on с примесью
class MyClass extends WithActions(HTMLElement) {
  @on('click') clickHandler() {}
}

Указанный декоратор может быть определен следующим образом:
// у нас может быть несколько обработчиков с одинаковыми именами,
// поэтому используется Symbol
// https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Global_Objects/Symbol
const handler = Symbol('handler')
function on(eventName) {
  return (method, context) => {
    context.metadata = { [handler]: eventName }
    return method
  }
}
class MetadataLookupCache {
  // в качестве ключей используются объекты,
  // во избежание утечек памяти используется WeakMap
  // https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Global_Objects/WeakMap
  #map = new WeakMap()
  #name
  constructor(name) { this.#name = name }
  get(newTarget) {
    let data = this.#map.get(newTarget)
    if (data === undefined) {
      data = []
      let klass = newTarget
      while (klass !== null && !(this.#name in klass)) {
        for (const [name, { [this.#name]: eventName }] of Object.entries(klass[Symbol.metadata].instance.methods)) {
          if (eventName !== undefined) {
            data.push({ name, eventName })
          }
        }
        klass = klass.__proto__
      }
      this.#map.set(newTarget, data)
    }
    return data
  }
}
const handlersMap = new MetadataLookupCache(handler)
function WithActions(superClass) {
  return class C extends superClass {
    constructor(...args) {
      super(...args)
      const handlers = handlersMap.get(new.target, C)
      for (const { name, eventName } of handlers) {
        this.addEventListener(eventName, this[name].bind(this))
      }
    }
  }
}

@bound c примесью
@bound может быть использован следующим образом:
class C extends WithBoundMethod(Object) {
  #x = 1
  @bound method() { return this.#x }
}
const c = new C()
const m = c.method
m() // 1, а не TypeError

Реализация декоратора может выглядеть так:
const boundName = Symbol('boundName')
function bound(method, context) {
  context.metadata = { [boundName]: true }
  return method
}
const boundMap = new MetadataLookupCache(boundName)
function WithBoundMethods(superClass) {
  return class C extends superClass {
    constructor(...args) {
      super(...args)
      const names = boundMap.get(new.target, C)
      for (const { name } of names) {
        this[name] = this[name].bind(this)
      }
    }
  }
}

Обратите внимание, что MetadataLookupCache используется в обоих примерах. Также имейте ввиду, что это и следующее предложение предполагают использование некой стандартной библиотеки для добавления метаданных.
Вариант 2: декораторы метода init
Декоратор init: предназначен для случаев, когда требуется выполнить операцию по инициализации, но невозможно вызвать суперкласс/примесь. Он позволяет добавлять такие операции при выполнении конструктора.
@on c init
Использование:
class MyElement extends HTMLElement {
  @init: on('click') clickHandler()
}

Декоратор init: вызывается также, как декораторы методов, но возвращает пару { method, initialize }, где initialize вызывается с новым экземпляром в качестве значения this, без аргументов, и ничего не возвращает.
function on(eventName) {
  return (method, context) => {
    assert(context.kind === 'init-method')
    return { method, initialize() { this.addEventListener(eventName, method) } }
  }
}

@bound с init
init: также может использоваться для построения декоратора init: bound:
class C {
  #x = 1
  @init: bound method() { return this.#x }
}
const c = new C()
const m = c.method
m() // 1, а не TypeError

Декоратор @bound может быть реализован следующим образом:
function bound(method, { kind, name }) {
  assert(kind === 'init-method')
  return { method, initialize() { this[name] = this[name].bind(this) } }
}

Для более подробной информации об ограничениях использования, а также об открытых вопросах, которые разработчикам предстоит решить перед стандартизацией декораторов в JavaScript, обратитесь к тексту предложения по ссылке, приведенной в начале статьи.
На этом позвольте откланяться. Благодарю за внимание.
===========
Источник:
habr.com
===========

Похожие новости: Теги для поиска: #_razrabotka_vebsajtov (Разработка веб-сайтов), #_javascript, #_programmirovanie (Программирование), #_javascript, #_decorator, #_decorators, #_future, #_feature, #_proposal, #_dekorator (декоратор), #_dekoratory (декораторы), #_vozmozhnost (возможность), #_predlozhenie (предложение), #_oop, #_class, #_klass (класс), #_razrabotka_vebsajtov (
Разработка веб-сайтов
)
, #_javascript, #_programmirovanie (
Программирование
)
Профиль  ЛС 
Показать сообщения:     

Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы

Текущее время: 22-Май 07:35
Часовой пояс: UTC + 5