Los decoradores de JavaScript: Qué son y cuándo usarlos
On enero 26, 2022 by adminCon la introducción de ES2015+, y como la transpilación se ha convertido en algo habitual, muchos de vosotros os habréis encontrado con nuevas características del lenguaje, ya sea en código real o en tutoriales. Una de estas características que a menudo tiene a la gente rascándose la cabeza cuando se encuentran por primera vez son los decoradores de JavaScript.
Los decoradores se han hecho populares gracias a su uso en Angular 2+. En Angular, los decoradores están disponibles gracias a TypeScript, pero en JavaScript son actualmente una propuesta de fase 2, lo que significa que deberían formar parte de una futura actualización del lenguaje. Echemos un vistazo a lo que son los decoradores, y cómo se pueden utilizar para hacer su código más limpio y más fácilmente comprensible.
¿Qué es un decorador?
En su forma más simple, un decorador es simplemente una manera de envolver una pieza de código con otra – literalmente «decorándola». Este es un concepto que bien podría haber escuchado previamente como composición funcional, o funciones de orden superior.
Esto ya es posible en JavaScript estándar para muchos casos de uso, simplemente llamando a una función para envolver a otra:
function doSomething(name) { console.log('Hello, ' + name);}function loggingDecorator(wrapped) { return function() { console.log('Starting'); const result = wrapped.apply(this, arguments); console.log('Finished'); return result; }}const wrapped = loggingDecorator(doSomething);
Este ejemplo produce una nueva función – en la variable wrapped
– que puede ser llamada exactamente de la misma manera que la función doSomething
, y hará exactamente lo mismo. La diferencia es que hará algunos registros antes y después de llamar a la función envuelta:
doSomething('Graham');// Hello, Grahamwrapped('Graham');// Starting// Hello, Graham// Finished
Cómo utilizar los decoradores de JavaScript
Los decoradores utilizan una sintaxis especial en JavaScript, por la que van precedidos de un símbolo @
y se colocan inmediatamente antes del código que se está decorando.
Nota: en el momento de escribir esto, los decoradores están actualmente en forma de «Borrador de la Etapa 2», lo que significa que están en su mayoría terminados, pero todavía están sujetos a cambios.
Es posible utilizar tantos decoradores en la misma pieza de código como se desee, y se aplicarán en el orden en que se declaren.
Por ejemplo:
@log()@immutable()class Example { @time('demo') doSomething() { // }}
Esto define una clase y aplica tres decoradores – dos a la propia clase, y uno a una propiedad de la clase:
-
@log
podría registrar todos los accesos a la clase -
@immutable
podría hacer la clase inmutable – tal vez llame aObject.freeze
en las nuevas instancias -
@time
registrará el tiempo que tarda un método en ejecutarse y lo registrará con una etiqueta única.
En la actualidad, el uso de los decoradores requiere el soporte del transpilador, ya que ningún navegador actual o versión de Node tiene soporte para ellos todavía. Si estás usando Babel, esto se habilita simplemente usando el plugin transform-decorators-legacy.
Nota: el uso de la palabra «legacy» en este plugin es porque soporta la forma de Babel 5 de manejar los decoradores, que bien podría ser diferente de la forma final cuando sean estandarizados.
¿Por qué usar decoradores?
Mientras que la composición funcional ya es posible en JavaScript, es significativamente más difícil -o incluso imposible- aplicar las mismas técnicas a otras piezas de código (por ejemplo, clases y propiedades de clases).
La propuesta de decoradores añade soporte para decoradores de clases y propiedades que pueden ser utilizados para resolver estos problemas, y las futuras versiones de JavaScript probablemente añadirán soporte de decoradores para otras áreas problemáticas de código.
Los decoradores también permiten una sintaxis más limpia para aplicar estas envolturas alrededor de su código, resultando en algo que desvirtúa menos la intención real de lo que está escribiendo.
Diferentes tipos de decorador
En la actualidad, los únicos tipos de decorador que se soportan son en las clases y los miembros de las clases. Esto incluye propiedades, métodos, getters y setters.
Los decoradores no son en realidad más que funciones que devuelven otra función, y que son llamadas con los detalles apropiados del elemento que se está decorando. Estas funciones decoradoras se evalúan una vez cuando el programa se ejecuta por primera vez, y el código decorado se sustituye por el valor de retorno.
Decoradores de miembros de clase
Los decoradores de propiedades se aplican a un único miembro de una clase -ya sean propiedades, métodos, getters o setters. Esta función decoradora se llama con tres parámetros:
-
target
: la clase en la que está el miembro. -
name
: el nombre del miembro en la clase. -
descriptor
: el descriptor del miembro. Esto es esencialmente el objeto que se habría pasado a Object.defineProperty.
El ejemplo clásico utilizado aquí es @readonly
. Esto se implementa tan simple como:
function readonly(target, name, descriptor) { descriptor.writable = false; return descriptor;}
Literalmente actualizar el descriptor de la propiedad para establecer la bandera «escribible» a falso.
Esto se utiliza entonces en una propiedad de la clase como sigue:
class Example { a() {} @readonly b() {}}const e = new Example();e.a = 1;e.b = 2;// TypeError: Cannot assign to read only property 'b' of object '#<Example>'
Pero podemos hacer mejor que esto. En realidad podemos reemplazar la función decorada con un comportamiento diferente. Por ejemplo, vamos a registrar todas las entradas y salidas:
function log(target, name, descriptor) { const original = descriptor.value; if (typeof original === 'function') { descriptor.value = function(...args) { console.log(`Arguments: ${args}`); try { const result = original.apply(this, args); console.log(`Result: ${result}`); return result; } catch (e) { console.log(`Error: ${e}`); throw e; } } } return descriptor;}
Esto reemplaza todo el método con uno nuevo que registra los argumentos, llama al método original y luego registra la salida.
Nota que aquí hemos utilizado el operador spread para construir automáticamente un array a partir de todos los argumentos proporcionados, que es la alternativa más moderna al antiguo valor arguments
.
Podemos ver esto en uso de la siguiente manera:
class Example { @log sum(a, b) { return a + b; }}const e = new Example();e.sum(1, 2);// Arguments: 1,2// Result: 3
Notarás que hemos tenido que utilizar una sintaxis un poco divertida para ejecutar el método decorado. Esto podría cubrir un artículo entero por sí mismo, pero en resumen, la función apply
permite llamar a la función, especificando el valor this
y los argumentos para llamarla.
Subiendo de nivel, podemos hacer que nuestro decorador tome algunos argumentos. Por ejemplo, vamos a reescribir nuestro decorador log
de la siguiente manera:
function log(name) { return function decorator(t, n, descriptor) { const original = descriptor.value; if (typeof original === 'function') { descriptor.value = function(...args) { console.log(`Arguments for ${name}: ${args}`); try { const result = original.apply(this, args); console.log(`Result from ${name}: ${result}`); return result; } catch (e) { console.log(`Error from ${name}: ${e}`); throw e; } } } return descriptor; };}
Esto se vuelve más complejo ahora, pero cuando lo desglosamos tenemos esto:
- Una función,
log
, que toma un solo parámetro:name
. - Esta función devuelve entonces una función que es a su vez un decorador.
Este es idéntico al anterior decorador log
, excepto que hace uso del parámetro name
de la función exterior.
Este se utiliza entonces de la siguiente manera:
class Example { @log('some tag') sum(a, b) { return a + b; }}const e = new Example();e.sum(1, 2);// Arguments for some tag: 1,2// Result from some tag: 3
Directamente podemos ver que esto nos permite distinguir entre diferentes líneas de registro utilizando una etiqueta que hemos suministrado nosotros mismos.
Esto funciona porque la llamada a la función log('some tag')
es evaluada por el tiempo de ejecución de JavaScript directamente, y luego la respuesta de eso se utiliza como el decorador para el método sum
.
Decoradores de clase
Los decoradores de clase se aplican a toda la definición de la clase de una sola vez. La función decoradora se llama con un solo parámetro que es la función constructora que se está decorando.
Nótese que esto se aplica a la función constructora y no a cada instancia de la clase que se crea. Esto significa que si quieres manipular las instancias tienes que hacerlo tú mismo devolviendo una versión envuelta del constructor.
En general, estos son menos útiles que los decoradores de miembros de clase, porque todo lo que puedes hacer aquí lo puedes hacer con una simple llamada a una función exactamente de la misma manera. Cualquier cosa que hagas con ellos tiene que acabar devolviendo una nueva función constructora que sustituya al constructor de la clase.
Volviendo a nuestro ejemplo de registro, escribamos uno que registre los parámetros del constructor:
function log(Class) { return (...args) => { console.log(args); return new Class(...args); };}
Aquí estamos aceptando una clase como nuestro argumento, y devolviendo una nueva función que actuará como el constructor. Esto simplemente registra los argumentos y devuelve una nueva instancia de la clase construida con esos argumentos.
Por ejemplo:
@logclass Example { constructor(name, age) { }}const e = new Example('Graham', 34);// console.log(e);// Example {}
Podemos ver que la construcción de nuestra clase Ejemplo registrará los argumentos proporcionados y que el valor construido es efectivamente una instancia de Example
. Exactamente lo que queríamos.
Pasar parámetros a los decoradores de la clase funciona exactamente igual que para los miembros de la clase:
function log(name) { return function decorator(Class) { return (...args) => { console.log(`Arguments for ${name}: args`); return new Class(...args); }; }}@log('Demo')class Example { constructor(name, age) {}}const e = new Example('Graham', 34);// Arguments for Demo: argsconsole.log(e);// Example {}
Ejemplos del mundo real
Decoradores del núcleo
Hay una fantástica biblioteca llamada Core Decorators que proporciona algunos decoradores comunes muy útiles que están listos para ser usados ahora mismo. Estos generalmente permiten una funcionalidad común muy útil (por ejemplo, la sincronización de las llamadas a los métodos, las advertencias de depreciación, asegurando que un valor es de sólo lectura) pero utilizando la sintaxis mucho más limpia de los decoradores.
React
La biblioteca React hace muy buen uso del concepto de Componentes de Orden Superior. Estos son simplemente componentes de React que se escriben como una función, y que envuelven a otro componente.
Compra nuestro curso Premium: React The ES6 Way
Estos son un candidato ideal para usar como decorador, porque hay muy poco que cambiar para hacerlo. Por ejemplo, la biblioteca react-redux tiene una función, connect
, que se utiliza para conectar un componente React a un almacén Redux.
En general, esto se utilizaría de la siguiente manera:
class MyReactComponent extends React.Component {}export default connect(mapStateToProps, mapDispatchToProps)(MyReactComponent);
Sin embargo, debido a cómo funciona la sintaxis de los decoradores, esto puede ser reemplazado con el siguiente código para lograr exactamente la misma funcionalidad:
@connect(mapStateToProps, mapDispatchToProps)export default class MyReactComponent extends React.Component {}
MobX
La biblioteca MobX hace un amplio uso de los decoradores, lo que le permite marcar fácilmente los campos como Observable o Computed, y marcar las clases como Observers.
Summary
Los decoradores de los miembros de la clase proporcionan una muy buena manera de envolver el código dentro de una clase de una manera muy similar a como ya se puede hacer para las funciones independientes. Esto proporciona una buena manera de escribir algún código de ayuda simple que se puede aplicar a una gran cantidad de lugares de una manera muy limpia y fácil de entender.
El único límite para el uso de este tipo de instalaciones es su imaginación!
Deja una respuesta