Decoradores de JavaScript Qué son y cuándo usarlos

Aprende acerca de decoradores en JavaScript qué son, cómo funcionan, para qué son útiles, sus ventajas y desventajas, y cómo usarlos.

En este artículo, profundizaremos en los decoradores en JavaScript: qué son, cómo funcionan, para qué sirven y cómo usarlos. Cubriremos la composición de decoradores, los decoradores de parámetros, los decoradores asincrónicos, la creación de decoradores personalizados, el uso de decoradores en varios frameworks, las fábricas de decoradores y los pros y contras de los decoradores en JavaScript.

¿Qué son los decoradores en JavaScript?

Un decorador es una función que agrega un superpoder a un método existente. Permite la modificación del comportamiento de un objeto sin cambiar su código original, pero extendiendo su funcionalidad.

Diagrama que muestra función, decorador, función decorada

Los decoradores son excelentes para mejorar la legibilidad, mantenibilidad y reutilidad del código. En JavaScript, los decoradores son funciones que pueden modificar clases, métodos, propiedades e incluso parámetros. Proporcionan una forma de agregar comportamiento o metadatos a diferentes partes de tu código sin alterar el código fuente.

Los decoradores se utilizan típicamente con clases y se les antepone el símbolo @:

// Un decorador simplefunction log(target, key, descriptor) {  console.log(`Registrando la función ${key}`);  return descriptor;}class Ejemplo {  @log  saludar() {    console.log("¡Hola, mundo!");  }}const ejemplo = new Ejemplo();ejemplo.saludar(); // Registra "Registrando la función saludar" y "¡Hola, mundo!"

El código anterior demuestra cómo un decorador puede modificar el comportamiento de un método de clase al registrar un mensaje antes de la ejecución del método.

Composición de decoradores

Los decoradores tienen la potente característica de ser compuestos y anidados. Esto significa que podemos aplicar varios decoradores al mismo fragmento de código y se ejecutarán en un orden específico. Esto ayuda a construir aplicaciones complejas y modulares.

Un ejemplo de composición de decoradores

Veamos un caso de uso donde se aplican varios decoradores al mismo código. Consideremos una aplicación web en la que queremos restringir el acceso a ciertas rutas basándonos en la autenticación y niveles de autorización del usuario. Podemos lograr esto componiendo decoradores de esta manera:

@requireAuth@requireAdminclass PanelAdmin {  // ...}

Aquí, requireAuth y requireAdmin son decoradores que aseguran que el usuario esté autenticado y tenga privilegios de administrador antes de acceder al PanelAdmin.

Decoradores de parámetros

Los decoradores de parámetros nos permiten modificar los parámetros de los métodos. Son menos comunes que otros tipos de decoradores, pero pueden ser valiosos en ciertas situaciones, como la validación o transformación de los argumentos de una función.

Un ejemplo de decorador de parámetros

Aquí tienes un ejemplo de un decorador de parámetros que asegura que un parámetro de una función esté dentro de un rango especificado:

function validateParam(min, max) {  return function (target, key, index) {    const originalMethod = target[key];    target[key] = function (...args) {      const arg = args[index];      if (arg < min || arg > max) {        throw new Error(`El argumento en el índice ${index} está fuera del rango.`);      }      return originalMethod.apply(this, args);    };  };}class OperacionesMatematicas {  @validateParam(0, 10)  multiplicar(a, b) {    return a * b;  }}const math = new OperacionesMatematicas();math.multiplicar(5, 12); // Lanza un error

El código define un decorador llamado validateParam aplicado a un método llamado multiplicar en la clase OperacionesMatematicas. El decorador validateParam verifica si los parámetros del método multiplicar están dentro del rango especificado (0 a 10). Cuando el método multiplicar se llama con los argumentos 5 y 12, el decorador detecta que 12 está fuera del rango y lanza un error.

Decoradores asíncronos

Los decoradores asíncronos manejan operaciones asíncronas en las aplicaciones modernas de JavaScript. Son útiles cuando se trabaja con async/await y promesas.

Un ejemplo de decorador asíncrono

Considera un escenario en el que queremos limitar la frecuencia con la que se llama a un método en particular. Podemos crear el decorador @throttle:

function throttle(delay) {  let lastExecution = 0;  return function (target, key, descriptor) {    const originalMethod = descriptor.value;    descriptor.value = async function (...args) {      const now = Date.now();      if (now - lastExecution >= delay) {        lastExecution = now;        return originalMethod.apply(this, args);      } else {        console.log(`Método ${key} limitado.`);      }    };  };}class DataService {  @throttle(1000)  async fetchData() {    // Obtener datos del servidor  }}const dataService = new DataService();dataService.fetchData(); // Solo se ejecuta una vez por segundo

Aquí, el decorador definido throttle se aplica al método fetchData en la clase DataService. El decorador throttle asegura que el método fetchData solo se ejecute una vez por segundo. Si se llama con mayor frecuencia, el decorador registra un mensaje que indica que el método ha sido limitado.

Este código demuestra cómo los decoradores pueden controlar la frecuencia con la que se invoca un método, lo cual puede ser útil en escenarios como la limitación de solicitudes de API.

Creación de decoradores personalizados

Aunque JavaScript proporciona algunos decoradores integrados como @deprecated o @readonly, hay casos en los que necesitamos crear decoradores personalizados adaptados a los requisitos específicos de nuestro proyecto.

Los decoradores personalizados son funciones definidas por el usuario que modifican el comportamiento o las propiedades de clases, métodos, propiedades o parámetros en el código de JavaScript. Estos decoradores encapsulan y reutilizan funcionalidades específicas o garantizan determinadas convenciones de forma consistente en todo nuestro código.

Ejemplos de decoradores personalizados

Los decoradores se escriben con el símbolo @. Creemos un decorador personalizado que registre un mensaje antes y después de la ejecución de un método. Este decorador ayudará a ilustrar la estructura básica de los decoradores personalizados:

function logMethod(target, key, descriptor) {  const originalMethod = descriptor.value; // Guardar el método original  // Redefinir el método con un comportamiento personalizado  descriptor.value = function (...args) {    console.log(`Antes de llamar a ${key}`);    const result = originalMethod.apply(this, args);    console.log(`Después de llamar a ${key}`);    return result;  };  return descriptor;}class Ejemplo {  @logMethod  saludar() {    console.log("¡Hola, mundo!");  }}const ejemplo = new Ejemplo();ejemplo.saludar();

En este ejemplo, hemos definido el decorador logMethod, que envuelve el método saludar de la clase Ejemplo. El decorador registra un mensaje antes y después de la ejecución del método, mejorando el comportamiento del método saludar sin modificar su código fuente.

Veamos otro ejemplo: un decorador personalizado @measureTime que registra el tiempo de ejecución de un método:

function measureTime(target, key, descriptor) {  const originalMethod = descriptor.value;  descriptor.value = function (...args) {    const start = performance.now();    const result = originalMethod.apply(this, args);    const end = performance.now();    console.log(`Tiempo de ejecución para ${key}: ${end - start} milisegundos`);    return result;  };  return descriptor;}class Timer {  @measureTime  heavyComputation() {    // Simula una computación pesada    for (let i = 0; i < 1000000000; i++) {}  }}const timer = new Timer();timer.heavyComputation(); // Registra el tiempo de ejecución

El código anterior define un decorador personalizado llamado measureTime y lo aplica a un método dentro de la clase Timer. Este decorador mide el tiempo de ejecución del método decorado. Cuando llamamos al método heavyComputation, el decorador registra el tiempo de inicio, realiza el cálculo, registra el tiempo de finalización, calcula el tiempo transcurrido y lo registra en la consola.

Este código demuestra cómo los decoradores agregan funcionalidad de monitoreo de rendimiento y de temporización a los métodos, lo cual puede ser valioso para optimizar el código e identificar cuellos de botella.

Casos de uso de las funcionalidades del decorador personalizado

Los decoradores personalizados pueden proporcionar diversas funcionalidades como validación, autenticación, registro o medición de rendimiento. Aquí tienes algunos casos de uso:

  • Validación. Podemos crear decoradores para validar los argumentos de los métodos, asegurando que cumplan con criterios específicos, como se muestra en el ejemplo anterior con la validación de parámetros.
  • Autenticación y autorización. Los decoradores se pueden utilizar para hacer cumplir el control de acceso y las reglas de autorización, lo que nos permite asegurar rutas o métodos.
  • Caché. Los decoradores pueden implementar mecanismos de caché para almacenar y recuperar datos de manera eficiente, reduciendo cálculos innecesarios.
  • Registro. Los decoradores pueden registrar llamadas a métodos, métricas de rendimiento o errores, lo que ayuda a la depuración y al monitoreo.
  • Memoización. Los decoradores de memoización pueden almacenar en caché los resultados de una función para entradas específicas, mejorando el rendimiento de cálculos repetitivos.
  • Mecanismo de reintento. Podemos crear decoradores que vuelvan a intentar automáticamente un método cierto número de veces en caso de fallos.
  • Manipulación de eventos. Los decoradores pueden activar eventos antes y después de la ejecución de un método, lo que permite arquitecturas orientadas a eventos.

Decoradores en diferentes frameworks

Los frameworks y bibliotecas de JavaScript, como Angular, React y Vue.js, tienen sus convenciones para el uso de decoradores. Comprender cómo funcionan los decoradores en estos frameworks nos ayuda a construir aplicaciones mejores.

Angular: uso extensivo de decoradores

Angular, un framework frontend completo, utiliza ampliamente los decoradores para definir diversas áreas de componentes, servicios y más. Aquí tienes algunos decoradores en Angular:

  • @Component. Se utiliza para definir un componente, especificando metadatos como el selector, la plantilla y los estilos del componente:

    @Component({  selector: "app-example",  template: "<p>Componente de ejemplo</p>",})class ExampleComponent {}
  • @Injectable. Marca una clase como un servicio que puede inyectarse en otros componentes y servicios:

    @Injectable()class ExampleService {}
  • @Input y @Output. Estos decoradores nos permiten definir propiedades de entrada y salida para los componentes, lo que facilita la comunicación entre componentes padre e hijos:

    @Input() title: string;@Output() notify: EventEmitter<string> = new EventEmitter();

Los decoradores de Angular mejoran la organización del código, lo que facilita la construcción de aplicaciones complejas con una arquitectura clara y estructurada.

React: componentes de orden superior

React es una biblioteca de JavaScript muy popular. No tiene decoradores nativos de la misma manera que Angular. Sin embargo, React introdujo un concepto conocido como componentes de orden superior (HOCs), los cuales funcionan como una forma de decorador. Los HOCs son funciones que toman un componente y devuelven un nuevo componente mejorado. Sirven para reutilizar código, abstraer el estado y manipular las propiedades.

Aquí tienes un ejemplo de un HOC que registra cuando se renderiza un componente:

function withLogger(WrappedComponent) {  return class extends React.Component {    render() {      console.log("Renderizando", WrappedComponent.name);      return <WrappedComponent {...this.props} />;    }  };}const EnhancedComponent = withLogger(MyComponent);

En este ejemplo, withLogger es un componente de orden superior que registra la renderización de cualquier componente que envuelve. Es una forma de mejorar los componentes con un comportamiento adicional sin alterar su código fuente.

Vue.js: opciones de componentes con decoradores

Vue.js es otro popular framework de JavaScript para construir interfaces de usuario. Aunque Vue.js no admite nativamente los decoradores, algunos proyectos y bibliotecas nos permiten usar decoradores para definir opciones de componentes.

Aquí tienes un ejemplo de cómo definir un componente Vue utilizando la biblioteca vue-class-component con decoradores:

javascriptCopiar códigoimport { Component, Prop, Vue } from 'vue-class-component';@Componentclass MyComponent extends Vue {  @Prop() title: string;  data() {    return { message: 'Hola, mundo!' };  }}

En este ejemplo, el decorador @Component se usa para definir un componente Vue, y el decorador @Prop se usa para hacer una prop en el componente.

Fábricas de decoradores

Las fábricas de decoradores son funciones que devuelven funciones decoradoras. En lugar de definir directamente un decorador, creamos una función que genera decoradores basados en los argumentos que pasamos. Esto hace posible personalizar el comportamiento de los decoradores, haciéndolos altamente versátiles y reutilizables.

La estructura general de una fábrica de decoradores se ve así:

function decoratorFactory(config) {  return function decorator(target, key, descriptor) {    // Personaliza el comportamiento del decorador basado en el argumento 'config'.    // Modifica el 'descriptor' u toma otras acciones según sea necesario.  };}

Aquí, decoratorFactory es la función de fábrica de decoradores que acepta un argumento de config. Devuelve una función decoradora, que puede modificar el target, key o descriptor basado en la configuración proporcionada.

Vamos a intentar otro ejemplo: una fábrica de decoradores que registra mensajes con diferentes niveles de gravedad:

function logWithSeverity(severity) {  return function (target, key, descriptor) {    const originalMethod = descriptor.value;    descriptor.value = function (...args) {      console.log(`[${severity}] ${key} llamado`);      return originalMethod.apply(this, args);    };  };}class Logger {  @logWithSeverity("INFO")  info() {    // Registra un mensaje informativo  }  @logWithSeverity("ERROR")  error() {    // Registra un mensaje de error  }}const logger = new Logger();logger.info(); // Registra "[INFO] info llamado"logger.error(); // Registra "[ERROR] error llamado"

En el código anterior, se utilizan decoradores personalizados para mejorar los métodos dentro de la clase Logger. Estos decoradores son creados por una fábrica de decoradores llamada logWithSeverity. Cuando se aplican a los métodos, registran mensajes con niveles de gravedad específicos antes de ejecutar el método original. En este caso, los métodos info y error de la clase Logger están decorados para registrar mensajes con niveles de gravedad INFO y ERROR respectivamente. Cuando llamamos a estos métodos, el decorador registra mensajes que indican la llamada al método y sus niveles de gravedad.

Este código demuestra cómo las fábricas de decoradores pueden crear decoradores personalizables para agregar comportamiento a los métodos, como el registro, sin alterar el código fuente.

Casos de uso práctico de las fábricas de decoradores

Las fábricas de decoradores son particularmente útiles para crear decoradores con diferentes configuraciones, condiciones o comportamientos. Aquí tienes algunos casos de uso práctico para las fábricas de decoradores:

  • Decoradores de validación. Podemos crear una fábrica de decoradores de validación para generar decoradores que validen condiciones específicas para los parámetros de los métodos. Por ejemplo, una fábrica de decoradores @validateParam puede hacer cumplir diferentes reglas para diferentes parámetros, como valores mínimos y máximos:

    function validateParam(min, max) {  return function (target, key, descriptor) {    // Valida el parámetro usando los valores 'min' y 'max'.  };}class MathOperations {  @validateParam(0, 10)  multiply(a, b) {    return a * b;  }}
  • Decoradores de registro. Las fábricas de decoradores pueden generar decoradores de registro con diferentes niveles o destinos de registro. Por ejemplo, podemos crear una fábrica de decoradores @logWithSeverity que registre mensajes con diferentes niveles de gravedad:

    function logWithSeverity(severity) {  return function (target, key, descriptor) {    // Registra mensajes con el 'severity' especificado.  };}class Logger {  @logWithSeverity("INFO")  info() {    // Registra mensajes informativos.  }  @logWithSeverity("ERROR")  error() {    // Registra mensajes de error.  }}
  • Decoradores condicionales. Las fábricas de decoradores nos permiten crear decoradores condicionales que apliquen el comportamiento decorado solo en ciertas circunstancias. Por ejemplo, podríamos crear un @conditionallyExecute decorador factory que verifique una condición antes de ejecutar el método:

    function conditionallyExecute(shouldExecute) {  return function (target, key, descriptor) {    if (shouldExecute) {      // Ejecutar el método.    } else {      // Omitir la ejecución.    }  };}class Example {  @conditionallyExecute(false)  someMethod() {    // Ejecutar condicionalmente este método.  }}

Beneficios de las fábricas de decoradores

Algunos de los beneficios de las fábricas de decoradores incluyen:

  • Configurabilidad. Las fábricas de decoradores nos permiten definir decoradores con varias configuraciones, adaptándolos a diferentes casos de uso.
  • Reusabilidad. Una vez que hemos creado una fábrica de decoradores, podemos reutilizarla en todo nuestro código, generando un comportamiento consistente.
  • Código limpio. Las fábricas de decoradores ayudan a mantener nuestro código limpio encapsulando un comportamiento específico y promoviendo una estructura más modular.
  • Dynamicidad. La naturaleza dinámica de las fábricas de decoradores las hace adaptables para aplicaciones complejas con requisitos variables.

Ventajas y desventajas de los decoradores en JavaScript

Los decoradores en JavaScript, aunque poderosos, tienen sus propias ventajas y desventajas que los desarrolladores deben tener en cuenta.

Ventajas de optimización de los decoradores en JavaScript

  • Reusabilidad de código. Los decoradores promueven la reutilización de código para preocupaciones comunes que atraviesan el código en diferentes lugares. En lugar de escribir la misma lógica en múltiples sitios, podemos encapsularla en un decorador y aplicarlo donde sea necesario. Reduce la duplicación de código, facilitando el mantenimiento y las actualizaciones.
  • Legibilidad. Los decoradores pueden mejorar la legibilidad del código al separar las preocupaciones. Cuando los decoradores se utilizan para administrar registros, validaciones u otras funcionalidades no esenciales, se vuelve más fácil centrarse en la lógica principal de la clase o el método.
  • Modularidad. Los decoradores promueven la modularidad en nuestro código. Podemos crear y mantener independientemente decoradores, y agregar o eliminar funcionalidades sin afectar la implementación principal.
  • Optimización del rendimiento. Los decoradores pueden optimizar el rendimiento al permitirnos almacenar en caché llamadas costosas a funciones, como se ve en los decoradores de memorización. Esto puede reducir significativamente el tiempo de ejecución cuando las mismas entradas producen las mismas salidas.
  • Pruebas y depuración. Los decoradores pueden ser útiles para las pruebas y la depuración. Podemos crear decoradores que registren las llamadas a los métodos y sus argumentos, lo que facilita la identificación y solución de problemas durante el desarrollo y la solución de problemas en producción.

Desventajas de optimización de los decoradores en JavaScript

  • Overhead. El uso de decoradores puede introducir overhead en nuestro código si aplicamos múltiples decoradores a la misma función o clase. Cada decorador puede traer código adicional que se ejecuta antes o después de la función original. Esto puede afectar el rendimiento, especialmente en aplicaciones críticas en tiempo.
  • Complejidad. A medida que nuestro código crece, el uso de decoradores puede agregar complejidad. Los decoradores a menudo implican encadenar múltiples funciones juntas, y entender el orden de ejecución puede volverse desafiante. También puede ser más complejo depurar dicho código.
  • Mantenimiento. Si se utilizan de manera excesiva, los decoradores pueden dificultar el mantenimiento del código. Los desarrolladores deben tener cuidado de no crear decoradores excesivos, lo cual puede generar confusión y dificultades para seguir las modificaciones del comportamiento.
  • Soporte limitado en navegadores. Los decoradores en JavaScript siguen siendo una propuesta y no están completamente soportados en todos los navegadores. Para utilizar decoradores en producción, es posible que necesitemos depender de transpiladores como Babel, lo que puede agregar complejidad adicional a nuestro proceso de construcción.

Conclusión

Este artículo ha brindado una exploración detallada de los decoradores en JavaScript. Los decoradores son funciones que mejoran el comportamiento de métodos, clases, propiedades o parámetros existentes de manera limpia y modular. Se utilizan para agregar funcionalidad o metadatos al código sin alterar su origen.

Con los conocimientos proporcionados aquí, utiliza los decoradores prudentemente en el desarrollo de JavaScript.

Puedes obtener más información sobre el desarrollo en curso de los decoradores en JavaScript leyendo la Propuesta de Decoradores TC39 en GitHub.

Preguntas frecuentes sobre los Decoradores en JavaScript

¿Qué son los decoradores en JavaScript?

Los decoradores son una característica propuesta en JavaScript que te permite agregar metadatos o comportamiento a clases, métodos y propiedades. Se aplican utilizando la sintaxis @decorator.

¿Por qué son útiles los decoradores en JavaScript?

Los decoradores ayudan a separar las preocupaciones y mejorar la legibilidad del código. Te permiten agregar características o funcionalidades a tu código sin sobrecargar la lógica principal de tus clases.

¿Cuáles son algunos casos de uso comunes para los decoradores en JavaScript?

Los decoradores se pueden utilizar para diversos propósitos, como logging, validación, autorización, almacenamiento en caché e inyección de dependencias. Son particularmente útiles en frameworks como Angular y TypeScript.

¿Cuáles son algunas bibliotecas o frameworks populares que utilizan decoradores?

Angular es un conocido framework que utiliza decoradores de manera extensiva para definir componentes, servicios y más. Mobx, una biblioteca de gestión de estado, también utiliza decoradores para definir datos observables.

¿Existen alternativas a los decoradores para lograr funcionalidades similares en JavaScript?

Aunque los decoradores son una forma conveniente de agregar metadatos y comportamiento, se pueden lograr resultados similares utilizando funciones de orden superior, mixins y otros patrones de diseño en JavaScript.

Comparte este artículo


Leave a Reply

Your email address will not be published. Required fields are marked *