How to deeply understand decorators in JavaScript

1ab081cbca9453814614c4f20039f88c.png

Created by Midjourney, future developer

A decorator is a way to dynamically add functionality while the code is running, it can be used to modify the behavior of a class or function. In other words, it allows us to add some extra functionality to a function or class at runtime without changing the original code.

concept

A decorator is a way to dynamically add functionality while the code is running. It is usually used to modify the behavior of a class or function to achieve the following functions:

  • extend the functionality of an existing class or function;

  • Modify the properties of a class or function;

  • Transform a class or function into a different form, such as a singleton pattern.

In JavaScript, there are many ways to implement decorators, the most common of which are using decorator functions and class decorators.

Using a decorator function

You can modify the behavior of a class or function by using a decorator function that receives an incoming class or function as an argument and returns the modified class or function. For example, the following example demonstrates how to add a logging function to a class using a decorator function:

function addLogFunction(cls) {
  // 向传入的类的原型上添加 log 方法
  cls.prototype.log = function(msg) {
    console.log(`[${new Date().toISOString()}] ${msg}`);
  };
  return cls;
}

@addLogFunction
class MyClass {
  constructor() {}
}

const myObj = new MyClass();
myObj.log('hello'); // 输出:[2023-03-29T06:00:00.000Z] hello

In this example, the function addLogFunction receives a class as an argument, and in the function adds a method to the class's prototype object. Then return the modified class. Use the decorator function @addLogFunction to declare the MyClass class, which is equivalent to executing MyClass = addLogFunction(MyClass). When the MyClass object is instantiated, log information can be output by calling the log method. So the execution order of the code is:

  • First, pass MyClass as a parameter to the addLogFunction function;

  • Then, in the addLogFunction function, a log method is added to the prototype of MyClass;

  • Then, return the modified MyClass class and assign it to the MyClass variable;

  • Finally, create an instance myObj of MyClass, and call its log method to output a message with a time stamp.

Therefore, the execution result of the code myObj.log('hello') is: output the message [2023-03-29T06:00:00.000Z] hello with a timestamp.

Using class decorators

A class decorator is a class used to modify a class, which can modify the behavior of the class, static properties, prototype properties, etc. Class decorators can receive three parameters:

  • constructor;

  • the name of the class;

  • The description object of the class.

The following is sample code for adding a static property to a class using a class decorator:

function addVersion(cls) {
  // 向传入的类添加 version 静态属性
  cls.version = '1.0';
  return cls;
}

@addVersion
class MyClass {}

console.log(MyClass.version); // 输出 1.0

In this example, the class decorator receives a constructor as an argument, adds a static property version to this constructor, and returns the modified constructor. Use the decorator function @addVersion to declare the MyClass class, which is equivalent to executing MyClass = addVersion(MyClass). This makes it possible to access the value of the static property version by calling MyClass.version. So the execution order of the code is:

  • First, pass MyClass as a parameter to the addVersion function;

  • Then, in the addVersion function, a static attribute version is added to MyClass;

  • Then, return the modified MyClass class and assign it to the MyClass variable;

  • Finally, the value of the version attribute is output by calling MyClass.version, which outputs 1.0.

Therefore, the execution result of the code MyClass.version is: output 1.0.

Common decorator application scenarios

Here are some common scenarios for using decorators:

Route request method decorator

function routeMethod(method) {
  // 返回一个函数,接收目标类、属性名和属性描述符作为参数
  return function(target, key, descriptor) {
    // 创建目标类的 routes 属性(如果不存在)
    target.routes = target.routes || {};
    // 将当前方法的请求方法(method)添加到目标类的 routes 属性中
    target.routes[key] = method;
    // 返回属性描述符
    return descriptor;
  };
}

class UserController {
  // 使用装饰器语法 @routeMethod('GET'),将 getUser 方法的请求方法设置为 GET
  @routeMethod('GET')
  getUser(id) {
    // ...
  }
  // 使用装饰器语法 @routeMethod('DELETE'),将 deleteUser 方法的请求方法设置为 DELETE
  @routeMethod('DELETE')
  deleteUser(id) {
    // ...
  }
}

console.log(UserController.routes);
// 输出:{getUser: "GET", deleteUser: "DELETE"}

In this example, the decorator function @routeMethod is used to add request methods to the getUser and deleteUser methods in the UserController class. The decorator function routeMethod receives a request method as a parameter and returns a decorator function that adds the request method to the target method. In the UserController class, use the decorator syntax @routeMethod('GET') and @routeMethod('DELETE') to declare the getUser and deleteUser methods, and set their corresponding request methods to GET and DELETE. Finally, view the request method corresponding to each method by outputting the routes attribute of the UserController class.

Thus, this example demonstrates how to use decorator functions to add extra functionality to methods in a class while keeping the code clean and easy to maintain.

Singleton pattern decorator

The singleton pattern is a common design pattern that ensures that a class has only one instance and provides a global access point to access that instance. In JavaScript, the singleton pattern can be implemented using the decorator pattern.

function singleton(cls) {
  // 定义一个 instance 变量,用于保存类的实例
  let instance;
  // 返回一个新的匿名函数
  return function() {
    // 如果实例不存在,则创建新的实例
    if (!instance) {
      instance = new cls(...arguments);
    }
    // 返回实例
    return instance;
  };
}

@singleton
class MyClass {
  constructor(val) {
    this.val = val;
  }
}

const a = new MyClass(1);
const b = new MyClass(2);
console.log(a === b); // 输出 true

The function singleton takes a class as an argument and returns a new function. This function first defines an instance variable for saving instances of the class. Then return a new anonymous function, each time the function is called, check whether the instance exists, if not, create a new instance and save it in the instance variable, and return the instance. In this way, you can ensure that there is only one instance of a class.

When defining the MyClass class, use the decorator syntax @singleton to call the function singleton, so that the MyClass class is decorated as a singleton pattern. Finally, verify the implementation of the singleton pattern by creating two instances a and b of the MyClass class and outputting their comparison results.

Therefore, this example demonstrates how to use the decorator pattern to implement the singleton pattern. The decorator function modifies the class when it is defined, so that only one instance of this class can be created, and this instance can be accessed globally.

Automatically bind this decorator

In JavaScript, due to the pointing problem of this, it is often necessary to manually bind the this value of the function to ensure that the this inside the function refers to the expected object. In order to avoid repeatedly writing this binding code, you can use the decorator pattern to automatically bind this function.

function autobind(_, _2, descriptor) {
  // 从属性描述符中获取原方法 fn、configurable、enumerable
  const { value: fn, configurable, enumerable } = descriptor;
  // 返回新的属性描述符对象
  return {
    configurable,
    enumerable,
    // 定义 getter 函数来绑定 this 值并返回绑定后的方法
    get() {
      const boundFn = fn.bind(this);
      // 将绑定后的方法重新定义为当前属性的值
      Object.defineProperty(this, key, {
        value: boundFn,
        configurable: true,
        writable: true,
      });
      return boundFn;
    },
  };
}

class MyComponent {
  constructor(props) {
    this.props = props;
  }
  // 使用 @autobind 装饰器来自动绑定 this 值
  @autobind
  handleClick() {
    console.log(this.props);
  }
}

The function autobind is a decorator function, which is used to realize the function of automatically binding this. It receives three parameters, where the descriptor parameter represents the property descriptor of the decorated method. Inside the function body, first obtain the three attributes of the original method fn, configurable and enumerable from the attribute descriptor. Then, a new property descriptor object is returned that contains a getter function that binds the this value and returns the bound method.

In the MyComponent class, use the decorator syntax @autobind to call the function autobind, so as to realize the function of automatically binding this of the handleClick method. Use the @autobind decorator to convert the handleClick method into a getter function, and bind the original method to the current instance object in this function, thereby realizing the function of automatically binding this.

Therefore, this example demonstrates how to use the decorator pattern to automatically bind this function, which can reduce redundant code in the code and improve the readability and maintainability of the code.

logging

Decorators can be used to record logs, including printing function call information, function execution time and other information.

Logging is a common development requirement, which can help developers better understand the operation of the program, and find and solve potential problems. In JavaScript, you can use the decorator pattern to implement logging functionality.

function log(target, name, descriptor) {
  // 保存原方法的引用
  const originalMethod = descriptor.value;
  // 重新定义方法
  descriptor.value = function (...args) {
      // 输出函数调用信息和参数
      console.log(`Function ${name} called with ${args}`);
      // 记录函数执行开始时间
      const start = performance.now();
      // 调用原方法
      const result = originalMethod.apply(this, args);
      // 计算函数执行时间
      const duration = performance.now() - start;
      // 输出函数执行时间
      console.log(`Function ${name} completed in ${duration}ms`);
      // 返回原方法的执行结果
      return result;
    };
    // 返回重新定义后的属性描述符
    return descriptor
  }

  class MyClass {
    // 使用 @log 装饰器来记录日志
    @log
    myMethod(arg1, arg2) {
      return arg1 + arg2;
    }
  }

  const obj = new MyClass();
  obj.myMethod(1, 2); // 输出:
  // Function myMethod called with 1,2
  // Function myMethod completed in 0.013614237010165215ms

The function log is a decorator function used to implement the logging function. It receives three parameters, where target represents the prototype object of the class to which the decorated method belongs, name represents the name of the decorated method, and descriptor represents the attribute descriptor of the decorated method. Inside the function body, first save the reference of the original method, then redefine the method, record the function call information, parameters, execution time and other information in this new method, and return the execution result of the original method.

In the MyClass class, use the decorator syntax @log to call the function log to implement the logging function of the myMethod method. Use the @log decorator to convert the myMethod method into a new method, and record the function call information, parameters, execution time and other information in this new method to achieve the logging function.

Therefore, this example demonstrates how to use the decorator pattern to implement the logging function. By redefining the method in the decorator function, the logging and function calls are separated, thereby improving the readability and maintainability of the code.

certified

Decorators can also be used to check a user's authentication status and permissions to prevent unauthorized users from accessing sensitive data or performing operations.

In Web applications, authentication and authorization are very important security mechanisms, they are used to verify the user's identity, and grant the user access to resources and perform operations. In JavaScript, you can use the decorator pattern to implement authentication and authorization functions.

The following is a sample code that uses the decorator pattern to implement authentication and authorization functions:

function authorization(target, name, descriptor) {
  // 保存原方法的引用
  const originalMethod = descriptor.value;
  
  // 重新定义方法
  descriptor.value = function(...args) {
        // 检查用户是否已认证
        if (!this.isAuthenticated()) {
          console.error('Access denied! Not authenticated');
          return;
        }
        // 检查用户是否有权限访问该端点
        if (!this.hasAccessTo(name)) {
          console.error(`Access denied! User does not have permission to ${name}`);
          return;
        }
        // 调用原方法并返回执行结果
        return originalMethod.apply(this, args);
      };
      // 返回重新定义后的属性描述符
      return descriptor;
   }

  class MyApi {
    // 实现身份验证功能
    isAuthenticated() {
      // 执行身份验证检查
      return true;
    }
    // 实现授权功能
    hasAccessTo(endpoint) {
      // 执行授权检查
      return true;
    }
    // 使用 @authorization 装饰器来进行身份验证和授权
    @authorization
    getUsers() {
      // 返回用户数据
    }
    // 使用 @authorization 装饰器来进行身份验证和授权
    @authorization
    deleteUser(id) {
      // 删除指定 ID 的用户
    }
  }

The function authorization is a decorator function used to implement authentication and authorization functions. It receives three parameters, where target represents the prototype object of the class to which the decorated method belongs, name represents the name of the decorated method, and descriptor represents the attribute descriptor of the decorated method. Inside the function body, first save the reference of the original method, then redefine the method, and check the user's authentication status and access rights in this new method, and return an error message when the user is not authorized or authenticated.

In the MyApi class, use the decorator syntax @authorization to call the function authorization to implement the authentication and authorization functions of the getUsers and deleteUser methods. Use the @authorization decorator to transform the getUsers and deleteUser methods into a new method where the user's authentication status and access rights are checked, improving application security.

Therefore, this example demonstrates how to use the decorator pattern to implement authentication and authorization functions. By redefining the method in the decorator function, the authentication and authorization logic is separated from the implementation of the method, thereby improving the readability and readability of the code. maintainability.

cache

Decorators can also be used to cache the execution results of functions to avoid double computation. Specifically, a decorator function can be used to store the function's input parameters as keys and the function's execution results as values ​​in an object. When the function is called, first check whether there is a corresponding result in the cache, if so, return the result directly, otherwise execute the function and store the result in the cache.

The following is a sample code that uses decorators to implement caching:

// 定义一个装饰器函数,实现函数缓存
function memoize(target, name, descriptor) {
  const originalMethod = descriptor.value; // 保存原始方法
  const cache = new Map(); // 创建缓存

  descriptor.value = function (...args) {
    const cacheKey = args.toString(); // 生成缓存键
    if (cache.has(cacheKey)) { // 如果缓存中已存在结果,直接返回
      console.log(`cache hit: ${cacheKey}`);
      return cache.get(cacheKey);
    }
    const result = originalMethod.apply(this, args); // 调用原始方法
    console.log(`cache miss: ${cacheKey}`); // 输出缓存未命中
    cache.set(cacheKey, result); // 将结果加入缓存
    return result;
  };
  return descriptor;
}

class MyMath {
  @memoize
  calculate(num) {
    console.log('calculate called');
    return num * 2;
  }
}

const math = new MyMath();
console.log(math.calculate(10)); // 输出:
// calculate called
// cache miss: 10
// 20
console.log(math.calculate(10)); // 输出:
// cache hit: 10
// 20

In this code, we first define the memoize decorator function, which receives three parameters target, name and descriptor, representing the target class, target method name and method attribute descriptor respectively. Inside the memoize function, we save the original method originalMethod and create a cache of type Map.

Next, we replace the original method with a new function. This new function first converts the input parameter args to a key cacheKey of type string, and checks whether this key exists in the cache. If it exists, directly return the corresponding result in the cache, otherwise call the original method originalMethod and save the result in the cache.

Finally, we return the modified property descriptor. The calculate method in the MyMath class uses the memoize decorator to realize the function of caching the calculation results. When calling the calculate method, if the parameters are the same, the result in the cache will be returned directly, avoiding repeated calculations.

Aspect Oriented Programming

Aspect-oriented programming (AOP) is a programming idea that divides the function of the program into multiple different aspects (facets), and implements these aspects by adding new functions at runtime without modifying the original code.

Decorators can be used to implement AOP by dynamically adding functionality without changing the original code. For example, decorators can be used to add logging, performance statistics, and more to a function without modifying the original function's implementation. This makes AOP a flexible and extensible programming style that can help developers better manage and maintain program complexity.

// 定义装饰器 validate,接收被装饰的方法的 target、name、descriptor 三个参数
function validate(target, name, descriptor) {
  const originalMethod = descriptor.value;

  // 修改 descriptor.value 属性为新函数,该函数会验证所有参数是否为非空字符串
  descriptor.value = function (...args) {
      const isValid = args.every(arg => typeof arg === 'string' && arg.length > 0);
      if (!isValid) {
        console.error('Invalid arguments');
        return;
      }
      return originalMethod.apply(this, args);
    };

  // 返回修改后的 descriptor 对象
  return descriptor;
}

// 定义 MyForm 类
class MyForm {
  // 在 submit 方法上使用 @validate 装饰器,表示该方法需要被验证参数
  @validate
  submit(name, email, message) {
    // 提交表单的逻辑
  }
}

// 创建 MyForm 的实例 form,并调用 submit 方法,但是传入的第一个参数是空字符串,不符合验证规则
const form = new MyForm();
form.submit('', '[email protected]', 'Hello world');

// Output: Invalid arguments

This code shows how to use decorators to implement AOP (Aspect-oriented Programming, aspect-oriented programming). AOP is a programming paradigm that allows functionality to be added without modifying the original code.

In this example, we define a decorator function validate, which receives three parameters: the target, name, and descriptor of the decorated method. The decorator function modifies the decorated method to implement parameter validation.

Inside the validate function, we first get the original implementation of the decorated method (ie descriptor.value), and then define a new function to replace the original implementation. The new function will first verify whether all parameters are non-empty strings, and if any parameter does not meet the requirements, it will output an error message and return.

Then, we assign the new function as a new method implementation to the descriptor.value property and return the modified descriptor object.

In the MyForm class, we decorate the submit method with the @validate decorator. This means that the method needs to be validated to ensure that the parameters passed in meet the requirements.

Finally, we create an instance form of the MyForm class and call the submit method. Since the first parameter is an empty string, which does not meet the validation rules, an error message will be output: "Invalid arguments".

reversible decorator

Decorators can also be applied in reversible scenarios, for example, you can add a reversible decorator to modify the behavior of a function. In simple terms, this means that you can undo or undo the changes made by the decorator at any time. This approach can make the code more flexible and maintainable.

// 定义一个可逆转的装饰器
function reverse(target, name, descriptor) {
  const originalMethod = descriptor.value;

  descriptor.value = function (...args) {
    args.reverse();
    return originalMethod.apply(this, args);
  };
  return descriptor;
}

class MyMath {
  @reverse
  calculate(num1, num2) {
    return num1 + num2;
  }
}

const math = new MyMath();
console.log(math.calculate(1, 2)); // 输出:3
console.log(math.calculate.reversed(1, 2)); // 输出:3

This code implements a reversible decorator function that is applied to the calculate method, which takes two arguments and returns their sum. After the decorator is applied, when the method is called, the parameters passed in will be sorted in reverse order, and then their sum will be calculated. Also, you can call the original method without the decorator by adding .reversed after the calculate method.

automatic type checking

Decorators can be used for automatic type checking, for example, you can add a decorator to ensure that function parameters are of the correct type. This is very helpful for ensuring the correctness of the code and avoiding errors. Beginners can use decorators to learn how to implement type checking in code to avoid runtime errors caused by type mismatches.

// 定义一个装饰器函数,用于类型检查
function checkType(expectedType) {
  return function(target, name, descriptor) {
    const originalMethod = descriptor.value;

    // 重写原函数,加入类型检查逻辑
    descriptor.value = function (...args) {
      const invalidArgs = args.filter(arg => typeof arg !== expectedType);
      if (invalidArgs.length > 0) {
        console.error(`Invalid arguments: ${invalidArgs}`);
        return;
      }
      return originalMethod.apply(this, args);
    };
    return descriptor;
  }
}

// 应用类型检查装饰器的类及函数
class MyMath {
  @checkType('number')
  add(num1, num2) {
    return num1 + num2;
  }
}

// 创建 MyMath 实例,并使用装饰器增加的类型检查功能
const math = new MyMath();
math.add(1, '2'); // 输出:Invalid arguments: 2

This code is a decorator for automatic type checking, by adding a decorator to the method to check whether the type of the incoming parameter is correct. This decorator returns a function, which in turn returns a decorator. When the method is invoked, the decorator checks whether the type of the incoming parameter is the expected type, and outputs an error message if it is not.

In this example, @checkType('number') is applied to the add method, so when the add method is called, the decorator will check whether the two parameters passed in are both numeric types, and if not, it will output an error information.

Finish

There are many other uses of decorators, such as error handling, data collection, retrying, etc. By using decorators, we can make our code more clean, concise and easy to maintain. At the same time, decorators also help to modularize different functions, making the code structure clearer and easier to expand.

Although decorators have been officially added to the ECMAScript standard, not all browsers support them. Therefore, when using decorators, we need to use tools such as Babel to convert the decorators into regular JavaScript code to ensure that our code can run normally in various environments.

In short, decorators are a very powerful and flexible language feature that can greatly simplify our code, while also improving the readability and maintainability of the code. Whether in web development, desktop application, or mobile application development, decorators are very useful tools that deserve our in-depth study and mastery.

That’s all for today’s sharing, thank you for reading, I hope it can help you, it’s not easy to create articles, if you like my sharing, don’t forget to like and forward it, so that more people in need can see it, and finally don’t forget to pay attention "Front-end experts", your support will be the biggest motivation for me to share, and I will continue to output more content in the future, so stay tuned.

Guess you like

Origin blog.csdn.net/Ed7zgeE9X/article/details/129891163