Bom dia amigos!
Apresento a vossa atenção uma tradução adaptada da nova proposta (setembro de 2020) relativa ao uso de decoradores em JavaScript, com uma pequena explicação do que está a acontecer.
Esta proposta foi feita pela primeira vez há cerca de 5 anos e passou por várias mudanças significativas desde então. Está (ainda) na segunda fase de consideração.
Se você nunca ouviu falar de decoradores antes ou se deseja atualizar seus conhecimentos, recomendo que leia os seguintes artigos:
Então, o que é um decorador? Um decorador é uma função chamada em um elemento de uma classe (campo ou método) ou na própria classe durante sua definição, que envolve ou substitui o elemento (ou classe) por um novo valor (retornado pelo decorador).
Um campo de classe decorado é tratado como um invólucro de um getter / setter, permitindo que você recupere / atribua (altere) um valor a esse campo.
Os decoradores também podem anotar um membro da classe com metadados. Metadados são uma coleção de propriedades de objetos simples adicionadas por decoradores. Eles estão disponíveis como um conjunto de objetos aninhados na propriedade [Symbol.metadata].
Sintaxe
A sintaxe do decorador, além do prefixo @ (@decoratorName), assume o seguinte:
- As expressões do decorador são limitadas ao encadeamento de variáveis (vários decoradores podem ser usados), acessando a propriedade com., Mas não com [] e chamando com ()
- Não apenas as definições de classe podem ser decoradas, mas também seus elementos (campos e métodos)
- Decoradores de classe são especificados após exportação e padrão
Não existem regras especiais para definir decoradores; qualquer função pode ser usada como tal.
Detalhes semânticos
O decorador é avaliado em três etapas:
- A expressão do decorador (o que vier após @) é avaliada junto com os nomes das propriedades calculadas
- O decorador é chamado (como uma função) durante a definição da classe, depois que os métodos são avaliados, mas antes que o construtor e o protótipo sejam combinados
- O decorador é aplicado (altera o construtor e o protótipo) apenas uma vez após a chamada
1. Decoradores de computação
Decoradores são avaliados como expressões junto com nomes de propriedades computadas. Isso acontece da esquerda para a direita e de cima para baixo. O resultado do decorador é armazenado em um tipo de variável local que é chamada (usada) após a definição da classe ser concluída.
2. Ligar para decoradores
O decorador é chamado com dois argumentos: o elemento encapsulado e, opcionalmente, o objeto de contexto.
Elemento empacotado: primeiro parâmetro
O primeiro argumento que o decorador envolve é o que decoramos (desculpe a tautologia):
- Quando se trata de um método simples, método de inicialização, getter ou setter: a função correspondente
- Se for sobre a aula: a própria aula
- Se for sobre o campo: um objeto com duas propriedades:
- get: uma função sem parâmetros que é chamada com um receptor, que é um objeto que retorna o valor que contém
- set: uma função que recebe um parâmetro (novo valor), que é chamada com um receptor sendo o objeto passado, e retorna indefinido
Objeto de contexto: segundo parâmetro
O objeto de contexto - o objeto passado ao decorador como o segundo argumento - contém as seguintes propriedades:
- kind: tem um dos seguintes valores:
- "Classe"
- "Método"
- "Método Init"
- "Getter"
- "Normatizador"
- "Campo"
- nome:
- campo ou método público: nome - string ou chave de propriedade de caractere
- campo ou método privado: nenhum
- classe: ausente
- isStatic:
- campo ou método estático: verdadeiro
- campo ou método de instância: false
- classe: ausente
O "destino" (construtor ou protótipo) não é passado para os decoradores de campo ou método pelo motivo de que (o "destino") ainda não foi construído no momento em que o decorador é chamado.
Valor de retorno
O valor de retorno depende do tipo de decorador:
- classe: nova classe
- método, getter ou setter: nova função
- campo: um objeto com três propriedades:
- pegue
- conjunto
- inicializar: uma função chamada com o mesmo argumento definido, retornando o valor usado para inicializar a variável. Esta função é chamada quando a configuração do armazenamento subjacente depende do inicializador de campo ou definição do método
- método init: um objeto com duas propriedades:
- método: uma função que substitui um método
- inicializar: uma função sem argumentos, cujo valor de retorno é ignorado e que é chamada com o objeto recém-criado como o receptor
3. Usando decoradores
Os decoradores são aplicados após serem chamados. Os estágios intermediários do algoritmo de trabalho do decorador não podem ser corrigidos - a classe recém-criada fica inacessível até que todos os decoradores dos métodos e campos de instância sejam aplicados.
Os decoradores de classe são chamados após a aplicação dos decoradores de campo e método.
Finalmente, são aplicados decoradores de campos estáticos.
Semântica de decoradores de campo
Um decorador de campo de classe é um par getter / setter para um campo privado. Portanto, o código:
function id(v) { return v }
class C {
@id x = y
}
tem a seguinte semântica:
class C {
// # -
#x = y
get x() { return this.#x }
set x(v) { this.#x = v }
}
Os decoradores de campo se comportam como campos privados. O código a seguir lançará uma exceção TypeError porque estamos tentando acessar "y" antes de adicioná-lo à instância:
class C {
@id x = this.y
@id y
}
new C // TypeError
O par getter / setter são métodos de objeto normais, que não são enumeráveis (não enumeráveis, se preferir) como outros métodos. Os campos privados que ele contém são adicionados um a um, junto com os inicializadores, como campos privados comuns.
Metas de design
- Deve ser tão fácil usar os decoradores integrados quanto escrever seus próprios
- Decoradores só devem ser aplicados a objetos decorados, sem efeitos colaterais.
Casos de aplicação
- Armazenamento de metadados em classes e métodos
- Converter um campo em acessador
- Encapsulando um método ou classe (esse uso de decoradores é um pouco semelhante ao proxy de objeto)
Exemplos de
Exemplos de implementação e uso de decoradores.
@logged
O decorador @logged imprime mensagens no console sobre o início e o fim da execução do método. Existem outros decoradores populares que agrupam funções como: @deprecated. debounce, @memoize, etc.
Usando:
// .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 pode ser implementado em JavaScript como um decorador. Um decorador é uma função chamada com um argumento que contém o elemento a ser decorado. Este elemento pode ser um método, getter ou setter. Os decoradores podem ser chamados com um segundo argumento, o contexto, entretanto, neste caso, não precisamos dele.
O valor retornado pelo decorador substitui o elemento empacotado. Para métodos, getters e setters, o valor de retorno é a função que os substitui.
// 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
}
O resultado da transpilação do exemplo dado pode ser assim:
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})
Observe que getters e setters são decorados separadamente. Acessores (propriedades computadas) não são combinados como nas cláusulas anteriores.
@defineElement
Elementos personalizados HTML (elementos personalizados, parte de componentes da web) permitem que você crie seus próprios elementos HTML. O registro de elementos é feito usando customElements.define . Veja como registrar um elemento usando decoradores:
import { defineElement } from './defineElement.js'
@defineElement('my-class')
class MyClass extends HTMLElement { }
As classes podem ser decoradas junto com métodos e acessores.
// defineElement.mjs
export function defineElement(name, options) {
return klass => {
customElements.define(name, klass, options); return klass
}
}
O decorador recebe argumentos que ele mesmo usa, portanto, é implementado como uma função que retorna outra função. Você pode pensar nisso como uma "fábrica de decoradores": depois de passar argumentos, você obtém um decorador diferente.
Decoradores adicionando metadados
Os decoradores podem fornecer metadados aos membros da classe adicionando uma propriedade de metadados ao objeto de contexto passado a eles. Todos os objetos contendo metadados são concatenados usando Object.assign e colocados na propriedade de classe [Symbol.metadata]. Por exemplo:
//
@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'
Observe que o formato de apresentação do objeto anotado é aproximado e pode ser mais refinado. A principal tarefa do exemplo é mostrar que uma anotação é apenas um objeto que não requer o uso de bibliotecas para ler ou gravar dados, ela é criada pelo sistema automaticamente.
O decorador em questão pode ser implementado assim:
function annotate(metadata) {
return (_, context) => {
context.metadata = metadata
return _
}
}
Cada vez que o decorador é chamado, um novo contexto é passado para ele, então a propriedade de metadados, desde que não seja indefinida, é incluída em [Symbol.metadata].
Observe que os metadados adicionados à própria classe, e não ao seu método, não estão disponíveis para decoradores declarados na classe. Adicionar metadados a uma classe ocorre no construtor depois de chamar todos os decoradores "internos" para evitar perda de dados.
@monitorados
O decorador @tracked observa o campo da classe e chama o método render quando o setter é chamado. Este padrão e padrões semelhantes são amplamente usados por várias estruturas para resolver o problema de re-renderização.
A semântica dos campos decorados sugere um wrapper getter / setter em torno de algum armazenamento de dados privado. @tracked pode envolver um par getter / setter para implementar a lógica de re-renderização:
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
Ao decorar um campo, o valor "empacotado" é um objeto com duas propriedades: funções get e set para gerenciar o armazenamento interno. Eles são projetados para ligar automaticamente a uma instância (usando call ()).
// tracked.mjs
export function tracked({ get, set }) {
return {
get,
set(value) {
if (get.call(this) !== value) {
set.call(this, value)
this.render()
}
}
}
}
Acesso limitado a campos e métodos privados
Às vezes, algum código fora da classe pode precisar acessar campos ou métodos privados. Por exemplo, para fornecer interoperabilidade entre duas classes ou para testar o código dentro de uma classe.
Decoradores tornam possível acessar campos e métodos privados. Essa lógica pode ser encapsulada em um objeto com chaves de referência privadas fornecidas conforme necessário.
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)
}
Observe que o exemplo acima é um tipo de hack que é mais fácil de implementar com construções como referenciar nomes privados com private.name ou estender o escopo de nomes privados com private / with . No entanto, mostra como esta proposta expande organicamente a funcionalidade 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)
}
}
@descontinuada
O decorador @deprecated imprime um aviso no console sobre o uso de campos, métodos ou acessadores obsoletos. Exemplo de uso:
import { deprecated } from './deprecated.mjs'
export class MyClass {
@deprecated field
@deprecated method() { }
otherMethod() { }
}
Para permitir que o decorador trabalhe com diferentes elementos da classe, o campo kind do contexto informa ao decorador o tipo de construção sintática reconhecida como obsoleta. Essa técnica também permite lançar exceções quando o decorador é usado em um contexto inválido, por exemplo: uma classe interna não pode ser marcada como reprovada porque não pode ter acesso negado.
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étodo que requerem pré-configuração
Alguns decoradores de método dependem da execução de código quando a classe é instanciada. Por exemplo:
- O decorador @on ('evento') para métodos de classe estende HTMLElement, que registra este método como um manipulador de eventos no construtor
- O decorador @bound é equivalente a this.method = this.method.bind (this) no construtor
Existem diferentes maneiras de usar os decoradores nomeados.
Opção 1: construtores e metadados
Esses decoradores são uma combinação de metadados e um mixin contendo operações de inicialização que são usadas no construtor.
@ ligado com um toque
class MyClass extends WithActions(HTMLElement) {
@on('click') clickHandler() {}
}
O decorador especificado pode ser definido assim:
// ,
// 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))
}
}
}
}
@ligado com um mixin
@bound pode ser usado assim:
class C extends WithBoundMethod(Object) {
#x = 1
@bound method() { return this.#x }
}
const c = new C()
const m = c.method
m() // 1, TypeError
A implementação do decorador pode ser assim:
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)
}
}
}
}
Observe que MetadataLookupCache é usado em ambos os exemplos. Além disso, lembre-se de que esta frase e as seguintes pressupõem o uso de algum tipo de biblioteca padrão para adicionar metadados.
Opção 2: decoradores de método iniciar
Decorador iniciar: destinado a casos em que uma operação de inicialização é necessária, mas não é possível chamar a superclasse / mixin. Ele permite que tais operações sejam adicionadas quando o construtor é executado.
@on c init
Usando:
class MyElement extends HTMLElement {
@init: on('click') clickHandler()
}
Decorador iniciar: Chamado apenas como decoradores de método, mas retorna um par {método, inicializar}, onde inicializar é chamado com uma nova instância como este valor, sem argumentos e não retorna nada.
function on(eventName) {
return (method, context) => {
assert(context.kind === 'init-method')
return { method, initialize() { this.addEventListener(eventName, method) } }
}
}
@bound com init
iniciar: também pode ser usado para construir um decorador iniciar: limite:
class C {
#x = 1
@init: bound method() { return this.#x }
}
const c = new C()
const m = c.method
m() // 1, TypeError
O decorador @bound pode ser implementado assim:
function bound(method, { kind, name }) {
assert(kind === 'init-method')
return { method, initialize() { this[name] = this[name].bind(this) } }
}
Para mais informações sobre as limitações de uso, bem como questões em aberto que os desenvolvedores devem resolver antes de padronizar os decoradores em JavaScript, consulte o texto da proposta no link fornecido no início do artigo.
Sobre isso, deixe-me sair. Obrigado pela atenção.