Decorators in JavaScript

Decorators in JavaScript

A decorator is a special type of declaration used to modify the behavior of a class, method, property, or parameter. Essentially, it is a function that adds extra functionality to the decorated target using the syntax of an @ symbol followed by the function name.

Basic Concepts of Decorators

  1. Decorators are currently a proposal in ECMAScript and require tools like Babel for transpilation to be used.
  2. Decorators are categorized into class decorators, method decorators, property decorators, and parameter decorators.
  3. Decorators are executed at compile time, not at runtime.

Basic Syntax Structure of Decorators

// Decorator function
function decorator(target, name, descriptor) {
  // Decorator logic
}

// Using decorators
@decorator
class MyClass {
  @decorator
  myMethod() {}
}

Implementation Steps for Class Decorators

  1. A class decorator receives one parameter: the constructor of the decorated class.
  2. It can return a new constructor to replace the original class.
  3. Example: Adding a static property to a class.
function addVersion(constructor) {
  constructor.version = '1.0.0';
  return constructor;
}

@addVersion
class MyClass {}

console.log(MyClass.version); // Output: 1.0.0

Detailed Implementation of Method Decorators

  1. Receives three parameters: the prototype of the target class, the method name, and the property descriptor.
  2. Can modify the behavior of the method, such as adding logging or validation.
  3. Must return a property descriptor object.
function log(target, name, descriptor) {
  const originalMethod = descriptor.value;
  
  // Modify method implementation
  descriptor.value = function(...args) {
    console.log(`Calling method ${name}, arguments:`, args);
    const result = originalMethod.apply(this, args);
    console.log(`Method ${name} execution result:`, result);
    return result;
  };
  
  return descriptor;
}

class Calculator {
  @log
  add(a, b) {
    return a + b;
  }
}

const calc = new Calculator();
calc.add(2, 3); // Will output call logs

Application of Property Decorators

  1. Receives two parameters: the prototype of the target class and the property name.
  2. Commonly used in metadata programming or property validation.
  3. Example: Automatically converting a property to uppercase.
function uppercase(target, propertyName) {
  let value = target[propertyName];
  
  // Define getter and setter
  const getter = () => value;
  const setter = (newVal) => {
    value = newVal.toUpperCase();
  };
  
  // Redefine the property
  Object.defineProperty(target, propertyName, {
    get: getter,
    set: setter,
    enumerable: true,
    configurable: true
  });
}

class Person {
  @uppercase
  name = 'john';
}

const person = new Person();
console.log(person.name); // Output: JOHN
person.name = 'alice';
console.log(person.name); // Output: ALICE

Execution Order of Multiple Decorators

  1. Decorators are executed sequentially, starting from the one closest to the target.
  2. Execution order: from top to bottom, from inner to outer.
  3. Example demonstrating the execution order.
function decoratorA(target, name, descriptor) {
  console.log('Executing decorator A');
  return descriptor;
}

function decoratorB(target, name, descriptor) {
  console.log('Executing decorator B');
  return descriptor;
}

class Example {
  @decoratorA
  @decoratorB
  method() {}
}
// Execution order: B first, then A

Practical Application Scenarios

  1. Automatic this binding: Solving this context issues in class methods.
function autoBind(target, name, descriptor) {
  const originalMethod = descriptor.value;
  return {
    configurable: true,
    enumerable: false,
    get() {
      // Bind the method to the current instance
      const boundFn = originalMethod.bind(this);
      Object.defineProperty(this, name, {
        value: boundFn,
        configurable: true,
        writable: true
      });
      return boundFn;
    }
  };
}
  1. Parameter validation decorator.
function validate(type) {
  return function(target, name, descriptor) {
    const originalMethod = descriptor.value;
    
    descriptor.value = function(...args) {
      if (typeof args[0] !== type) {
        throw new Error(`Parameter type error, expected type: ${type}`);
      }
      return originalMethod.apply(this, args);
    };
    
    return descriptor;
  };
}

class Validator {
  @validate('string')
  process(text) {
    return text.toUpperCase();
  }
}

Decorators provide powerful metaprogramming capabilities in JavaScript, enabling elegant implementation of cross-cutting concerns (such as logging, validation, performance monitoring, etc.), making the code more modular and maintainable.