Encountering AOP: Talking about decorators in JavaScript

  Hi, friends, everyone, welcome to follow my blog, I am Payne, and my blog address is https://qinyuanpei.github.io . Basically, I haven't updated the blog and official account much this month, so today I want to write a popular science article on the theme of modifiers in JavaScript. Why is the term "encounter" used? Because when you know that you can no longer encounter love, you can only expect to encounter things other than love; when you realize that love is just a small episode in life, you can only try to make up for the integrity of life. In past blogs, I have introduced AOP-related frameworks such as Spring.NET, Unity, AspectCore, etc. I have also introduced decorators in Python, Attributes in .NET, annotations in Java, etc. Wait. From my point of view, these are very similar concepts, so in this article today we are going to talk about AOP again! What? You said that there is actually AOP in JavaScript! Is this simply happier than any feature? And this starts with the protagonist of this article - the decorator in JavaScript.

What is a decorator?

Decorator   in JavaScript is a proposal of ES7. The current browser version does not support this feature, so the mainstream technical solution is to use Babel for translation. In fact, there are quite a few tools in the front-end tool chain. Of course, these are our future topics! The appearance of the decorator mainly solves the following two problems:

  • Shared methods between classes
  • Make modifications to a class and its methods during compile time

  The first point here doesn't seem to be significant, because after modularization in JavaScript, you only need to export it as a module to share methods between different parties. Of course, on the issue of modularity, the JavaScript community has carried forward a consistent tradition of confusion. Different specifications such as CommonJS, AMD, CMD, etc. emerge one after another. Fortunately, ES6 uses import and export to implement module functions. This is the current fact modular standard. The second point to pay attention to here, modifying the class and its methods during compile time , this can modify the class and its methods, which is very interesting! Note that the decorator here is Decorator , we immediately think of the decorator in Python, the decorator pattern, and the proxy pattern, so I believe it is not difficult for everyone to understand what I said here, and we are going to talk about AOP again. !

  So with all that said, what exactly do decorators in JavaScript look like? In fact, there is nothing mysterious about it. We have seen it in both Python and Java. The former is called a decorator, and the latter is called an annotation , that is, adding an @ symbol to a class or method, think of Spring's Controller, we probably know it looks like this:

/* 修饰类 */
@bar
class foo {}

/* 修饰方法 */
@bar
foo(){}

  OK, now everyone must think that this TM is simply plagiarizing Python, right? In order to avoid everyone becoming a superficial person, let's take a look at the following specific examples:

Decorative class

@setProp
class User {}

function setProp(target) {
    target.age = 30
}

console.log(User.age)

  This example shows how we assign values ​​to the User object through the decorator function setProp(). Why is it called a decorator function? Because this is a function, and JavaScript, like Python, is a programming language that supports functional programming, so everyone should not be surprised to see this, because the road to simplicity is the same. Well, notice that the SetProp() method has a parameter target. Because this method modifies the User class, its parameter is the User class. Obviously, it extends a property age for the User class and assigns it a value of 30. I believe some friends will wonder where this age is defined. I can only say that JavaScript is a magical language, everything is an object, and everything is a function. Now, when we reach the last sentence, it will output 30 because the decorator modifies the class.

  Now we try to modify this method. We hope that the value of the age attribute can be modified through the decorator, instead of making it a fixed value of 30, which involves the decorator function with parameters. The decorator function itself will receive three parameters, the first parameter is the object to be decorated, so in order to add a new parameter, we need to wrap the original function, you know? At this point I was very excited, because this TM is really exactly the same as Python. Well, following this strategy, we modify the original code and adjust it as follows:

@setProp(20)
class User {}

function setProp(value) {
    return function (target) {
        target.age = value
    }
}

console.log(User.age)

This difference can be clearly seen. When we use the decorator function setProp(), we now allow a parameter of 20 to be passed in. The result at this time is very obvious. This code will be as you wish. ground output 20.

Modification method

  Since decorators can decorate classes, can they decorate methods? The answer is of course yes. Because when a decorator modifies a class, the parameter of the decorator function is an object, that is, target, and when a decorator decorates a method, the parameter of the decorator function is a function. Isn't a function an object? Don't delegates in .NET also generate a class in the end? Isn't there a concept of function objects in Python? So, let's move on to an example:

class User {
    @readonly
    getName() {
        return 'Hello World'
    }
}

// readonly修饰函数,对方法进行只读操作
function readonly(target, name, descriptor) {
    descriptor.writable = false
    return descriptor
}

let u = new User()
// 尝试修改函数,在控制台会报错
u.getName = () => {
    return 'I will override'
}

In this example, we decorate the getName() method with the decorator function readonly() to make it a readonly method. We mentioned that the decorator function has three parameters, target refers to the decorated object, name refers to the name of the decorated object, and descriptor refers to the defineProperty of the decorated object. Because after setting the descriptor's writable property to false, this function cannot be overwritten and rewritten, so an error will be reported when trying to rewrite the method in the code; similarly, if we modify the descriptor's value property, the function can be rewrite.

Summarize

  I believe friends who are familiar with Python should know that there are a lot of built-in decorators in Python. For example, @property can make a method be called like an attribute, @staticmethod can make a method a static method, and @classmethod can make a method into a class method, etc. So, as a follower of Python, is there a similar concept in JavaSript? The answer is still yes! Ha ha. For details, you can refer to here: ES6 Decorator

AOP and decorators

  Friends who are familiar with my writing style should be able to guess what I am going to do next. Indeed, as a person with obsessive-compulsive disorder in some aspects, I have been sparing no effort to promote AOP to everyone, because I believe that AOP can really help you do a lot of things. For example, the simplest logging, perhaps in front-end projects, everyone is more accustomed to using console.log() to record logs, or even use alert(). After all, these things will not be displayed on the interface, so writing these things seems like Nothing wrong with that. But when you have AOP, why do you have to do such a thankless thing? An important reason why I wrote this article is that I saw that in the code of my front-end colleagues, a simple AOP was done using decorators, which was very much in my taste. How to do it specifically? Let's look at this code together:

class Bussiness {
    @log
    step1() {}

    @log
    step2() {}
}

function log(target,name,decriptor){
    var origin = descriptor.value;
    descriptor.value = function(){
      console.log('Calling function "${name}" with ', argumants);
      return origin.apply(null, arguments);
    };

    return descriptor;
}

  We just mentioned that the purpose of rewriting the method can be achieved by modifying the value attribute of the descriptor, so here is the way to modify the original method, and call console.log() to write a line of log before calling the original method. Indeed, it is such a bland line of code that rescues us from the quagmire. Imagine seeing a piece of code mixed with logging and business processes. Who would be in the mood to decipher the real meaning behind the code, not to mention how difficult it will be to delete these logs one day in the future. The basic idea of ​​AOP is to insert code snippets before and after code execution, because according to prototypal inheritance in JavaScript, we can easily extend the before and after functions for the Function type:

Function.prototype.before = function(beforefunc){
  var self = this;
  var outerArgs = Array.prototype.slice.call(arguments,1);
  return function{
    var innerArgs = Array.prototype.slice.call(arguments);
    beforefunc.apply(this,innerArgs);
    self.apply(this,outerArgs)
  };
};

Function.prototype.after = function(afterfunc){
  var self = this;
  var outerArgs = Array.prototype.slice.call(arguments,1);
  return function{
    var innerArgs = Array.prototype.slice.call(arguments);
    self.apply(this,outerArgs)
    afterfunc.apply(this,innerArgs);
  };
};

  Imagine that when we rewrite the value property of descriptor, we can specify its before() method and after() method at the same time, so the original code can continue to be rewritten as:

var func = function(){
    console.log('Calling function "${name}" with ', argumants);
    return origin.apply(null, arguments);
};

func.before(function(){
  console.log('Start calling function ${name}');
})();

func.after(function(){
  console.log('End calling function ${name}');
})();

  So, all the things that make you feel that it will increase the risk is from your inner fear, because you are not willing to try to change, this is real reuse, if Ctrl + C and Ctrl + V can be called reuse If so, I think everyone can call themselves an internet celebrity! This is not a joke, what could be simpler than writing a @log? Also, instead of subtracting two Date() objects all over the place, we can use a decorator to count the elapsed time of the code. Follow the brevity and start with the heart:

function time(){
  return function log(target,name,decriptor){
    var origin = descriptor.value;
    descriptor.value = function(){
      let beginTime = new Date();
      let result = origin.apply(null, arguments);
      let endTime = new Date();
      let time = endTime.getTime() - beginTime.getTime();
      console.log("Calling function '${name}' used '${time}' ms"); 
      return result;
    };

    return descriptor;
  };
}

@time
foo()

  For another example, our business requires that when users access related resources or perform related operations, they need to ensure that the user's status is logged in. Therefore, we inevitably use an if statement in the code to determine whether the user is logged in or not. Imagine that if all business code is written like this, there will be direct coupling between the two modules. Of course, we can say that this is the easiest way, because it takes care of the thinking and emotions of most people, but you can see Angular/Redux/ TypeScript and other projects are all full of modifiers. When a framework gradually becomes popular and becomes a trend, it seems that everyone immediately forgets one thing: originally, we all rejected these tricks and tricks. , but because of the popularity of the framework you have accepted this setting by default. So how would this logic be written using decorators?

class User {
    @checkLogin
    getUserInfo() {
        console.log('获取已登录用户的用户信息')
    }

    @checkLogin
    sendMsg() {
        console.log('发送消息')
    }
}

// 检查用户是否登录,如果没有登录,就跳转到登录页面
function checkLogin(target, name, descriptor) {
    let method = descriptor.value
    descriptor.value = function (...args) {
        //假想的校验方法,假设这里可以获取到用户名/密码
        if (validate(args)) {
            method.apply(this, args)
        } else {
            console.log('没有登录,即将跳转到登录页面...')
        }
    }
}
let u = new User()
u.getUserInfo()
u.sendMsg()

  Obviously, now we can avoid the direct coupling between modules, without repeating the if statement in each business method, and more importantly, through the modularization specification in JavaScript, we can extend the checkLogin method to more businesses Classes and their methods, and the only price is to add @checkLogin decoration to the method, you say, with such an elegant strategy, why are you unwilling to use it? In ASP.NET, we can authorize APIs and pages through the Authorize feature. Now it seems that this is a bit similar to the same purpose? Do you still feel this troublesome now?

Summary of this article

  This article takes the log interceptor (InterceptLog) in a front-end project as an introduction, and leads to a feature in the ES7 proposal: decorator. The appearance of decorators solves two problems: first, sharing methods between different classes; second, modifying classes and their methods during compile time . Although the decorator cannot be used directly in the browser at present, through translation tools such as Babel, we can already experience this feature in advance in the project, and I commend my colleagues in the front-end group. The decorator in JavaScript is similar to the decorator in Python, it can decorate the class and its methods. The decorator in JavaScript does not recommend decorating functions, because there is a problem of function promotion. If you must decorate a function, you can directly wrap the function according to the concept of higher-order functions. We can simplify our code through decorators. In this article, we have exemplified three AOP-related examples of logging, running time recording, and login checking. I hope you can gain something from this article.

  Finally, please allow the blogger to break the news, because to write a simple decorator, you need to install several Babel or even Webpack plugins. The code in this article has not been able to be used in the actual environment as of the time of writing this article. I can't blame it for running, because the front-end toolchain is too long. Of course, this can't be compared with Python with built-in decorators. This is really not a rant, I need an out-of-the-box feature Is it that hard? Life is too short, I use Python! (escape

Reference article

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=324514245&siteId=291194637