¡Buen dia amigos!
Les presento una traducción adaptada de la nueva propuesta (septiembre de 2020) sobre el uso de decoradores en JavaScript, con una pequeña explicación de lo que está sucediendo.
Esta propuesta se hizo por primera vez hace unos 5 años y ha sufrido varios cambios importantes desde entonces. Actualmente se encuentra (todavía) en la segunda etapa de consideración.
Si no ha oído hablar antes de los decoradores o quiere repasar sus conocimientos, le recomiendo que lea los siguientes artículos:
- Decoradores de JavaScript
- Una guía mínima para los decoradores de ECMAScript
- Decoradores de JavaScript desde cero
Entonces, ¿qué es un decorador? Un decorador es una función llamada en un elemento de una clase (campo o método) o en la propia clase durante su definición, que envuelve o reemplaza el elemento (o clase) con un nuevo valor (devuelto por el decorador).
Un campo de clase decorado se trata como un contenedor de un captador / definidor, lo que le permite recuperar / asignar (cambiar) un valor a ese campo.
Los decoradores también pueden anotar a un miembro de la clase con metadatos. Los metadatos son una colección de propiedades de objetos simples agregadas por decoradores. Están disponibles como un conjunto de objetos anidados en la propiedad [Symbol.metadata].
Sintaxis
La sintaxis del decorador, además del prefijo @ (@decoratorName), asume lo siguiente:
- Las expresiones de decorador se limitan al encadenamiento de variables (se pueden usar varios decoradores), acceder a la propiedad con., Pero no con [], y llamar con ()
- No solo se pueden decorar las definiciones de clases, sino también sus elementos (campos y métodos)
- Los decoradores de clase se especifican después de la exportación y por defecto
No hay reglas especiales para definir decoradores; cualquier función se puede utilizar como tal.
Detalles semánticos
El decorador se evalúa en tres pasos:
- La expresión del decorador (lo que sigue a @) se evalúa junto con los nombres de propiedad calculados
- El decorador se llama (como función) durante la definición de la clase, después de que se evalúan los métodos, pero antes de que se combinen el constructor y el prototipo.
- El decorador se aplica (cambia de constructor y prototipo) solo una vez después de la llamada
1. Decoradores informáticos
Los decoradores se evalúan como expresiones junto con los nombres de propiedad calculados. Esto sucede de izquierda a derecha y de arriba hacia abajo. El resultado del decorador se almacena en una especie de variable local que se llama (se usa) después de que se completa la definición de la clase.
2. Llamada a los decoradores
El decorador se llama con dos argumentos: el elemento envuelto y, opcionalmente, el objeto de contexto.
Elemento envuelto: primer parámetro
El primer argumento que envuelve el decorador es lo que decoramos (perdón por la tautología):
- Cuando se trata de un método simple, método de inicialización, getter o setter: la función correspondiente
- Si se trata de la clase: la clase en sí
- If about field: un objeto con dos propiedades:
- get: una función sin parámetros, que se llama con un receptor, que es un objeto que devuelve el valor que contiene
- conjunto: una función que toma un parámetro (nuevo valor), que se llama con un receptor que es el objeto pasado, y devuelve indefinido
Objeto de contexto: segundo parámetro
El objeto de contexto, el objeto pasado al decorador como segundo argumento, contiene las siguientes propiedades:
- kind: tiene uno de los siguientes valores:
- "Clase"
- "Método"
- "Método de inicialización"
- "Adquiridor"
- "Setter"
- "Campo"
- nombre:
- campo o método público: nombre - clave de propiedad de cadena o carácter
- campo o método privado: ninguno
- clase: ausente
- isStatic:
- campo o método estático: verdadero
- campo o método de instancia: falso
- clase: ausente
El "objetivo" (constructor o prototipo) no se pasa a los decoradores de campo o método porque (el "objetivo") aún no se ha construido en el momento en que se llama al decorador.
Valor devuelto
El valor de retorno depende del tipo de decorador:
- clase: nueva clase
- método, captador o definidor: nueva función
- campo: un objeto con tres propiedades:
- obtener
- conjunto
- initialize: una función llamada con el mismo argumento que el set, devolviendo el valor usado para inicializar la variable. Esta función se llama cuando la configuración del almacenamiento subyacente depende del inicializador de campo o la definición del método.
- método init: un objeto con dos propiedades:
- método: una función que reemplaza un método
- inicializar: una función sin argumentos, cuyo valor de retorno se ignora y que se llama con el objeto recién creado como receptor
3. Usar decoradores
Los decoradores se aplican después de que se llaman. Las etapas intermedias del algoritmo de trabajo del decorador no se pueden arreglar: la clase recién creada es inaccesible hasta que se aplican todos los decoradores de los métodos y campos de instancia.
Los decoradores de clase se llaman después de que se hayan aplicado los decoradores de campo y método.
Finalmente, se aplican decoradores de campo estático.
Semántica de los decoradores de campo
Un decorador de campo de clase es un par getter / setter para un campo privado. Por lo tanto el código:
function id(v) { return v }
class C {
@id x = y
}
tiene la siguiente semántica:
class C {
// # -
#x = y
get x() { return this.#x }
set x(v) { this.#x = v }
}
Los decoradores de campo se comportan como campos privados. El siguiente código generará una excepción TypeError porque estamos intentando acceder a "y" antes de agregarlo a la instancia:
class C {
@id x = this.y
@id y
}
new C // TypeError
El par getter / setter son métodos de objetos normales, que no son enumerables (no enumerables, por así decirlo) como otros métodos. Los campos privados que contiene se agregan uno por uno, junto con los inicializadores, como campos privados ordinarios.
Objetivos de diseño
- Debería ser tan fácil usar los decoradores integrados como escribir los suyos propios.
- Los decoradores solo deben aplicarse a objetos decorados sin efectos secundarios.
Casos de aplicación
- Almacenar metadatos en clases y métodos
- Convertir un campo en un descriptor de acceso
- Envolviendo un método o clase (este uso de decoradores es algo similar al proxy de objetos)
Ejemplos de
Ejemplos de implementación y uso de decoradores.
@logged
El decorador @logged imprime mensajes en la consola sobre el inicio y el final de la ejecución del método. Hay otros decoradores populares que envuelven funciones como: @deprecated. rebote, @memorizar, etc.
Utilizando:
// .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 se puede implementar en JavaScript como decorador. Un decorador es una función que se llama con un argumento que contiene el elemento a decorar. Este elemento puede ser un método, captador o definidor. Los decoradores se pueden llamar con un segundo argumento, el contexto, sin embargo, en este caso no lo necesitamos.
El valor devuelto por el decorador reemplaza el elemento envuelto. Para métodos, captadores y definidores, el valor de retorno es la función que los reemplaza.
// 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
}
El resultado de la transpilación del ejemplo dado puede verse así:
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})
Tenga en cuenta que los getters y setters se decoran por separado. Los descriptores de acceso (propiedades calculadas) no se combinan como en las cláusulas anteriores.
@defineElement
Elementos personalizados HTML (elementos personalizados, parte de componentes web) le permite crear sus propios elementos HTML. Los elementos se registran mediante customElements.define . A continuación, se explica cómo registrar un elemento mediante decoradores:
import { defineElement } from './defineElement.js'
@defineElement('my-class')
class MyClass extends HTMLElement { }
Las clases se pueden decorar junto con métodos y accesorios.
// defineElement.mjs
export function defineElement(name, options) {
return klass => {
customElements.define(name, klass, options); return klass
}
}
El decorador toma argumentos que usa él mismo, por lo que se implementa como una función que devuelve otra función. Puedes pensar en esto como una "fábrica de decoradores": después de pasar argumentos, obtienes un decorador diferente.
Decoradores agregando metadatos
Los decoradores pueden proporcionar metadatos a los miembros de la clase agregando una propiedad de metadatos al objeto de contexto que se les pasa. Todos los objetos que contienen metadatos se concatenan mediante Object.assign y se colocan en la propiedad de clase [Symbol.metadata]. Por ejemplo:
//
@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'
Tenga en cuenta que el formato de presentación del objeto anotado es aproximado y puede perfeccionarse aún más. La tarea principal del ejemplo es mostrar que una anotación es solo un objeto que no requiere el uso de bibliotecas para leer o escribir datos en ella, el sistema la crea automáticamente.
El decorador en cuestión se puede implementar así:
function annotate(metadata) {
return (_, context) => {
context.metadata = metadata
return _
}
}
Cada vez que se llama al decorador, se le pasa un nuevo contexto, luego la propiedad de metadatos, siempre que no esté indefinida, se incluye en [Symbol.metadata].
Tenga en cuenta que los metadatos agregados a la clase en sí, y no a su método, no están disponibles para los decoradores declarados en la clase. La adición de metadatos a una clase ocurre en el constructor después de llamar a todos los decoradores "internos" para evitar la pérdida de datos.
@tracked
El decorador @tracked observa el campo de la clase y llama al método render cuando se invoca al setter. Este patrón y patrones similares son ampliamente utilizados por varios marcos para resolver el problema de la reproducción.
La semántica de los campos decorados sugiere una envoltura de captador / definidor alrededor de algún almacén de datos privado. @tracked puede envolver un par getter / setter para implementar la lógica de re-renderizado:
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
Al decorar un campo, el valor "envuelto" es un objeto con dos propiedades: obtener y establecer funciones para administrar el almacenamiento interno. Están diseñados para vincularse automáticamente a una instancia (usando call ()).
// tracked.mjs
export function tracked({ get, set }) {
return {
get,
set(value) {
if (get.call(this) !== value) {
set.call(this, value)
this.render()
}
}
}
}
Acceso limitado a campos y métodos privados
A veces, es posible que algún código fuera de la clase necesite acceder a campos o métodos privados. Por ejemplo, para proporcionar interoperabilidad entre dos clases o para probar código dentro de una clase.
Los decoradores permiten acceder a campos y métodos privados. Esta lógica se puede encapsular en un objeto con claves de referencia privadas proporcionadas según sea necesario.
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)
}
Tenga en cuenta que el ejemplo anterior es un tipo de truco que es más fácil de implementar con construcciones como hacer referencia a nombres privados con private.name o ampliar el alcance de los nombres privados con private / with . Sin embargo, muestra cómo esta propuesta expande orgánicamente la funcionalidad existente.
// 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)
}
}
@obsoleto
El decorador @deprecated imprime una advertencia en la consola sobre el uso de campos, métodos o descriptores de acceso obsoletos. Ejemplo de uso:
import { deprecated } from './deprecated.mjs'
export class MyClass {
@deprecated field
@deprecated method() { }
otherMethod() { }
}
Para permitir que el decorador trabaje con diferentes elementos de la clase, el campo tipo del contexto informa al decorador del tipo de construcción sintáctica reconocida como obsoleta. Esta técnica también le permite lanzar excepciones cuando se usa un decorador en un contexto no válido, por ejemplo: una clase interna no se puede marcar como obsoleta porque no se le puede negar el acceso.
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`)
}
}
Decoradores de métodos que requieren configuración previa
Algunos decoradores de métodos se basan en ejecutar código cuando se crea una instancia de la clase. Por ejemplo:
- El decorador @on ('evento') para métodos de clase extiende HTMLElement, que registra este método como un controlador de eventos en el constructor
- El decorador @bound es equivalente a this.method = this.method.bind (this) en el constructor
Hay diferentes formas de utilizar los decoradores nombrados.
Opción 1: constructores y metadatos
Estos decoradores son una combinación de metadatos y un mixin que contiene operaciones de inicialización que se utilizan en el constructor.
@on con un toque
class MyClass extends WithActions(HTMLElement) {
@on('click') clickHandler() {}
}
El decorador especificado se puede definir así:
// ,
// 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 con un mixin
@bound se puede usar así:
class C extends WithBoundMethod(Object) {
#x = 1
@bound method() { return this.#x }
}
const c = new C()
const m = c.method
m() // 1, TypeError
La implementación del decorador podría verse así:
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)
}
}
}
}
Tenga en cuenta que MetadataLookupCache se usa en ambos ejemplos. Además, tenga en cuenta que esta oración y la siguiente asumen el uso de algún tipo de biblioteca estándar para agregar metadatos.
Opción 2: decoradores de métodos en eso
Decorador en eso: destinado a los casos en los que se requiere una operación de inicialización, pero no es posible llamar a la superclase / mixin. Le permite agregar tales operaciones cuando se ejecuta el constructor.
@on c init
Utilizando:
class MyElement extends HTMLElement {
@init: on('click') clickHandler()
}
Decorador en eso: se llama como los decoradores de métodos, pero devuelve un par {método, inicializar} donde se llama a inicializar con una nueva instancia como este valor, sin argumentos y no devuelve nada.
function on(eventName) {
return (method, context) => {
assert(context.kind === 'init-method')
return { method, initialize() { this.addEventListener(eventName, method) } }
}
}
@bound con init
en eso: también se puede usar para construir un decorador en eso: Unido:
class C {
#x = 1
@init: bound method() { return this.#x }
}
const c = new C()
const m = c.method
m() // 1, TypeError
El decorador @bound se puede implementar así:
function bound(method, { kind, name }) {
assert(kind === 'init-method')
return { method, initialize() { this[name] = this[name].bind(this) } }
}
Para obtener más información sobre las limitaciones de uso, así como las preguntas abiertas que los desarrolladores tienen que resolver antes de estandarizar los decoradores en JavaScript, consulte el texto de la propuesta en el enlace que se proporciona al comienzo del artículo.
Sobre esto, déjame despedirme. Gracias por su atención.