[Разработка веб-сайтов, JavaScript, Программирование] Будущее JavaScript: декораторы
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Доброго времени суток, друзья!
Представляю вашему вниманию адаптированный перевод нового варианта предложения (сентябрь 2020 г.), касающегося использования декораторов в JavaScript, с небольшими пояснениями относительно характера происходящего.
Впервые данное предложение прозвучало около 5 лет назад и с тех пор претерпело несколько значительных изменений. В настоящее время оно (по-прежнему) находится на второй стадии рассмотрения.
Если вы раньше не слышали о декораторах или хотите освежить свои знания, рекомендую ознакомиться со следующими статьями:
- Декораторы в JavaScript
- Минимальное руководство по декораторам ECMAScript
- Декораторы JavaScript с нуля
Итак, что такое декоратор? Декоратор (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
===========
Похожие новости:
- [Программирование микроконтроллеров, Разработка под Arduino, DIY или Сделай сам] Запускаем код для Arduino в браузере
- [Программирование, Машинное обучение] Введение: Соревнование от финансовой группы HOME CREDIT по определеню риска дефолта заемщика
- [FPGA, Программирование микроконтроллеров, Учебный процесс в IT, Компьютерное железо, Робототехника] Passcode Data Protection by Using FPGA and Verilog
- [Программирование, Компьютерное железо] Аппаратный эмулятор клавиатуры и мыши с интерфейсом USB
- [Программирование, C#] Поиск, устранение и предупреждение утечек памяти в C# .NET: 8 лучших практик (перевод)
- [Разработка веб-сайтов, JavaScript, Программирование, GitHub, Планшеты] Мой опыт разработки с использованием андроид устройства
- [Python, Программирование, Машинное обучение, DIY или Сделай сам] Пора избавляться от мышки или Hand Pose Estimation на базе LiDAR за 30 минут
- [PHP, Программирование] Производительность Composer 2.0 с JIT PHP 8 (перевод)
- [Разработка веб-сайтов, JavaScript, Angular, TypeScript] Taiga UI — библиотека компонентов под Angular, которую вам стоит попробовать
- [Python, Программирование] Приложение для конвертирования jpg файлов в pdf файл
Теги для поиска: #_razrabotka_vebsajtov (Разработка веб-сайтов), #_javascript, #_programmirovanie (Программирование), #_javascript, #_decorator, #_decorators, #_future, #_feature, #_proposal, #_dekorator (декоратор), #_dekoratory (декораторы), #_vozmozhnost (возможность), #_predlozhenie (предложение), #_oop, #_class, #_klass (класс), #_razrabotka_vebsajtov (
Разработка веб-сайтов
), #_javascript, #_programmirovanie (
Программирование
)
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 14:22
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Доброго времени суток, друзья! Представляю вашему вниманию адаптированный перевод нового варианта предложения (сентябрь 2020 г.), касающегося использования декораторов в JavaScript, с небольшими пояснениями относительно характера происходящего. Впервые данное предложение прозвучало около 5 лет назад и с тех пор претерпело несколько значительных изменений. В настоящее время оно (по-прежнему) находится на второй стадии рассмотрения. Если вы раньше не слышали о декораторах или хотите освежить свои знания, рекомендую ознакомиться со следующими статьями:
Итак, что такое декоратор? Декоратор (decorator) — это функция, вызываемая на элементе класса (поле или методе) или на самом классе в процессе его определения, оборачивающая или заменяющая элемент (или класс) новым значением (возвращаемым декоратором). Декорированное поле класса обрабатывается как обертка из геттера/сеттера, позволяющая извлекать/присваивать (изменять) значение этому полю. Декораторы также могут аннотировать элемент класса метаданными (metadata). Метаданные — это коллекция простых свойств объекта, добавленных декораторами. Они доступны как набор вложенных объектов в свойстве [Symbol.metadata]. Синтаксис Синтаксис декораторов, помимо префикса @ (@decoratorName), предполагает следующее:
Для определения декораторов не существует каких-либо специальных правил; любая функция может быть использована в качестве такового. Детали семантики Декоратор оценивается в три этапа:
1. Вычисление декораторов Декораторы оцениваются как выражения вместе с вычисляемыми именами свойств. Это происходит слева направо и сверху вниз. Результат декоратора сохраняется в своего рода локальную переменную, которая вызывается (используется) после завершения определения класса. 2. Вызов декораторов Декоратор вызывается с двумя аргументами: оборачиваемым элементом и, опционально, объектом контекста. Оборачиваемый элемент: первый параметр Первый аргумент, который оборачивается декоратором, это то, что мы декорируем (извиняюсь за тавтологию):
Объект контекста: второй параметр Объект контекста — объект, передаваемый декоратору в качестве второго аргумента — содержит следующие свойства:
«Target» (конструктор или прототип) не передается декораторам полей или методов по той причине, что она («цель») еще не сконструирована в момент вызова декоратора. Возвращаемое значение Возвращаемое значение зависит от типа декоратора:
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`) } } Декораторы методов, требующие предварительной настройки Некоторые декораторы методов основаны на выполнении кода при создании экземпляра класса. Например:
Существуют разные способы использования названных декораторов. Вариант 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 =========== Похожие новости:
Разработка веб-сайтов ), #_javascript, #_programmirovanie ( Программирование ) |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 14:22
Часовой пояс: UTC + 5