As the saying goes, clothes make the man. The little sister on the street likes to dress up themselves, so that you can not help but see a few eyes, this is the role of decoration.

1. Introduction

Decorators, a recent ECMA proposal, are class-related syntax for annotating or modifying classes and class methods. Decorators are also heavily used in languages such as Python and Java. Decorators are an important way to implement AOP (Aspect oriented) programming.

Here’s a simple example of using a decorator. This @readonly makes the count attribute read-only. As you can see, decorators greatly improve the brevity and readability of your code.

class Person {
    @readonly count = 0;
}
Copy the code

Since the browser does not yet support decorators, I have a simple configuration to use Parcel to make it work. You can clone the repository and then run all the examples in this article: Learn ES6

This article involves the knowledge of Object. DefineProperty, higher-order functions and so on. If you have not understood the relevant concepts before, it is recommended to read this article after understanding.

2. Decorator mode

Before we get into decorators, let’s start with the classic decorator pattern. The decorator pattern is a structural design pattern that allows new functionality to be added to an existing object without changing its structure, acting as a wrapper around an existing class.

In general, we should follow the principle of “more combination, less inheritance” in code design. Add additional responsibilities to an object dynamically through the decorator pattern. The decorator pattern is more flexible than subclassing in terms of adding functionality.

2.1 An example of League of Legends

When I was using Yasso “Face the Wind”, IT suddenly occurred to me that if I were to design yasso hero, how would I achieve it?

I had an idea that I would design a hero class first.

class Hero {
    attack(){}}Copy the code

Then, implement a Yasuo class that inherits from the Hero class.

class Yasuo extends Hero {
    attack() {
        console.log("Cut steel flash"); }}Copy the code

While I was thinking about this, my teammate had already hit Tai Lung, and my Yasso had a tai Lung buff on him. It occurred to me, how do I increase dragon buffs for heroes? How about adding a dragon buff?

Of course not very good, you know, the league of Legends dragon buff will increase the revenue.

Well, clever I have already thought of a way, it would be good to inherit again?

class BaronYasuo extends Yasuo {}
Copy the code

That’s impressive, but what if Yasso has other buffs? After all, there are red buffs, blue buffs, dragon buffs and so on in LOL, so there should be as many classes as there are.

Another way to think about it is if you think of buff as the clothes on our bodies. We wear different clothes for different seasons, and in winter, we even add more clothes. When the buff wears off, it’s the equivalent of taking the garment off. As shown in the figure below:

Clothes are decorative for people, buff for Yasso is just an enhancement. So, do you have any ideas? Yes, you can create a Buff class, pass in a hero class and get a new enhanced hero class.

class RedBuff extends Buff {
    constructor(hero) {
        this.hero = hero;
    }
    // The red buff deals additional damage
    extraDamage(){}attack() {
        return this.hero.attack() + this.extraDamage(); }}class BlueBuff extends Buff {
    constructor(hero) {
        this.hero = hero;
    }
    // Skill CD (minus 10%)
    CDR() {
        return this.hero.CDR() * 0.9; }}class BaronBuff extends Buff {
    constructor(hero) {
        this.hero = hero;
    }
    // The return speed to the city is halved
    backSpeed() {
        return this.hero.backSpeed * 0.5; }}Copy the code

After all buff classes have been defined, they can be applied directly to heroes. Does this look a lot cleaner? It looks a lot like a combination of functions.

const yasuo = new Yasuo();
const redYasuo = new RedBuff(yasuo); // Red buff yasuo
const blueYasuo = new BlueBuff(yasuo); // Blue buff Yasso
const redBlueYasuo = new BlueBuff(redYasuo); // Red and blue buff Yasso
Copy the code

3. ES7 decorators

Decorators are a proposal in ES7, currently in stage-2. The proposal address is JavaScript Decorators.

The decorator is very similar to the function compose and higher order functions we talked about earlier. Decorators use @ as an identifier and are placed before the code being decorated. In other languages, there are already sophisticated decorator schemes.

3.1 Decorators in Python

Let’s look at an example of a decorator in Python:

def auth(func):
    def inner(request,*args,**kwargs):
        v = request.COOKIES.get('user')
        if not v:
            return redirect('/login')
        return func(request, *args,**kwargs)
    return inner

@auth
def index(request):
    v = request.COOKIES.get("user")
    return render(request,"index.html", {"current_user":v})
Copy the code

The Auth decorator checks the cookie to see if the user is logged in. The auth function is a higher-order function that takes a func function as an argument and returns a new inner function.

The cookie is checked in the inner function to determine whether to jump back to the login page or continue executing the func function.

This auth decorator can be used on any function that requires permission verification. It is simple and non-intrusive.

3.2 JavaScript decorators

JavaScript decorators are similar to Python decorators in that they rely on object.defineProperty to decorate classes, class attributes, and class methods.

Using decorators makes it possible to implement certain functions without directly modifying the code, allowing true aspect oriented programming. This is similar to Proxy to some extent, but is much cleaner to use than Proxy.

Note: The decorator is currently stage-2, meaning the syntax may change later. There are already some plans for using decorators for functions, objects, etc. See Future Built-in Decorators

3.3 class decorators

When decorating a class, the decorator method typically takes a target class as an argument. Here is an example of adding the static property test to the target class:

const decoratorClass = (targetClass) = > {
    targetClass.test = '123'
}
@decoratorClass
class Test {}
Test.test; / / '123'
Copy the code

In addition to modifying the class itself, you can add new properties to the instance by modifying the stereotype. Here is an example of adding the speak method to the target class:

const withSpeak = (targetClass) = > {
    const prototype = targetClass.prototype;
    prototype.speak = function() {
        console.log('I can speak '.this.language);
    }
}
@withSpeak
class Student {
    constructor(language) {
        this.language = language; }}const student1 = new Student('Chinese');
const student2 = new Student('English');
student1.speak(); // I can speak Chinese

student2.speak(); // I can speak Englist
Copy the code

Using the properties of higher-order functions, you can also pass parameters to decorators to determine what to do with the class.

const withLanguage = (language) = > (targetClass) = > {
    targetClass.prototype.language = language;
}
@withLanguage('Chinese')
class Student {}const student = new Student();
student.language; // 'Chinese'
Copy the code

If you often write react-Redux code, you will also encounter situations where you need to map data from stores to components. Connect is a high-level component that receives two functions, mapStateToProps and mapDispatchToProps, as well as a component App, eventually returning an enhanced version of the component.

class App extends React.Component {
}
connect(mapStateToProps, mapDispatchToProps)(App)
Copy the code

With decorators, connect can be written more elegantly.

@connect(mapStateToProps, mapDispatchToProps)
class App extends React.Component {}Copy the code

3.4 Class property decorators

A class property decorator can be used in a class property, method, or get/set function. It usually takes three arguments:

  1. target: The class that is decorated
  2. name: The name of a class member
  3. descriptor: property descriptor, which the object passes toObject.defineProperty

There are a lot of interesting things you can do with class attribute decorators, such as the readonly example we started with:

function readonly(target, name, descriptor) {
  descriptor.writable = false;
  return descriptor;
}
class Person {
    @readonly name = 'person'
}
const person = new Person();
person.name = 'tom'; 
Copy the code

It can also be used to count the execution time of a function for later performance tuning.

function time(target, name, descriptor) {
    const func = descriptor.value;
    if (typeof func === 'function') {
        descriptor.value = function(. args) {
            console.time();
            const results = func.apply(this, args);
            console.timeEnd();
            returnresults; }}}class Person {
    @time
    say() {
        console.log('hello')}}const person = new Person();
person.say();
Copy the code

React’s well-known state management library, MOBX, also uses decorators to make class attributes observable for responsive programming.

import {
    observable,
    action,
    autorun
} from 'mobx'

class Store {
    @observable count = 1;
    @action
    changeCount(count) {
        this.count = count; }}const store = new Store();
autorun(() = > {
    console.log('count is ', store.count);
})
store.changeCount(10); // Changing count causes autorun to execute automatically.
Copy the code

3.5 Decorator combination

What if you want to use more than one decorator? Decorators are stackable, executing in sequence according to the distance from the class/property being decorated.

class Person {
    @time
    @log
    say(){}}Copy the code

In addition, in the decorator proposal, there is an example of a decorator that combines multiple decorators. We haven’t seen it in use yet.

Declare a composite decorator XYZ, which combines multiple decorators, by using decorator.

decorator @xyz(arg, arg2 {
  @foo @bar(arg) @baz(arg2)
}
@xyz(1.2) class C {}Copy the code

It’s the same thing as writing it this way.

@foo @bar(1) @baz(2)
class C {}Copy the code

4. What interesting things can decorators do?

4.1 Multiple Inheritance

Mixins can be used when implementing multiple inheritance in JavaScript, and the combination of decorators can even simplify the use of mixins even further.

The mixin method will receive a list of parent classes to decorate the target class with. Our ideal usage would look something like this:

@mixin(Parent1, Parent2, Parent3)
class Child {}
Copy the code

Similar to the previous implementation of multiple inheritance, you only need to copy the prototype properties and instance properties of the parent class.

A new Mixin class is created to copy all the properties from the mixins and targetClass.

const mixin = (. mixins) = > (targetClass) = > {
  mixins = [targetClass, ...mixins];
  function copyProperties(target, source) {
    for (let key of Reflect.ownKeys(source)) {
      if(key ! = ='constructor'&& key ! = ='prototype'&& key ! = ='name'
      ) {
        let desc = Object.getOwnPropertyDescriptor(source, key);
        Object.defineProperty(target, key, desc); }}}class Mixin {
    constructor(. args) {
      for (let mixin of mixins) {
        copyProperties(this.newmixin(... args));// Copy the instance properties}}}for (let mixin of mixins) {
    copyProperties(Mixin, mixin); // Copy static properties
    copyProperties(Mixin.prototype, mixin.prototype); // Copy the stereotype properties
  }
  return Mixin;
}

export default mixin
Copy the code

Let’s test this mixin method to see if it works.

class Parent1 {
    p1() {
        console.log('this is parent1')}}class Parent2 {
    p2() {
        console.log('this is parent2')}}class Parent3 {
    p3() {
        console.log('this is parent3')
    }
}
@mixin(Parent1, Parent2, Parent3)
class Child {
    c1 = () = > {
        console.log('this is child')}}const child = new Child();
console.log(child);
Copy the code

The resulting child object printed in the browser looks like this, proving that the mixin works.

Note: The Child class is the Mixin class.

Why create an extra Mixin class, you might ask? Why can’t you modify targetClass constructor directly? Didn’t we say that a Proxy can intercept Constructor?

Congratulations, you’ve thought of one possible use case for Proxy. Yes, it’s more elegant to use a Proxy here.

const mixin = (. mixins) = > (targetClass) = > {
    function copyProperties(target, source) {
        for (let key of Reflect.ownKeys(source)) {
          if( key ! = ='constructor'&& key ! = ='prototype'&& key ! = ='name'
          ) {
            let desc = Object.getOwnPropertyDescriptor(source, key);
            Object.defineProperty(target, key, desc); }}}for (let mixin of mixins) {
        copyProperties(targetClass, mixin); // Copy static properties
        copyProperties(targetClass.prototype, mixin.prototype); // Copy the stereotype properties
      }
      // Intercept the construct method and copy the instance properties
      return new Proxy(targetClass, {
        construct(target, args) {
          const obj = newtarget(... args);for (let mixin of mixins) {
              copyProperties(obj, new mixin()); // Copy the instance properties
          }
          returnobj; }}); }Copy the code

4.2 Anti-shaking and throttling

In the past, throttling functions have been used to optimize performance in frequently triggered scenarios. Let’s use the React component to bind scroll events as an example:

class App extends React.Component {
    componentDidMount() {   
        this.handleScroll = _.throttle(this.scroll, 500);
        window.addEveneListener('scroll'.this.handleScroll);
    }
    componentWillUnmount() {
        window.removeEveneListener('scroll'.this.handleScroll);
    }
    scroll(){}}Copy the code

When binding an event to a component, note that it should be unbound when the component is destroyed. Since the throttling function returns a new anonymous function, you have to save the anonymous function for later use in order to effectively unbind it.

Instead of having to manually set the throttle method for each event, we can simply add a decorator to the Scroll function.

const throttle = (time) = > {
    let prev = new Date(a);return (target, name, descriptor) = > {
        const func = descriptor.value;
        if (typeof func === 'function') {
            descriptor.value = function(. args) {
		        const now = new Date(a);if (now - prev > wait) {
			        fn.apply(this, args);
			        prev = new Date(a); } } } } }Copy the code

It’s a lot simpler to use.

class App extends React.Component {
    componentDidMount() {
        window.addEveneListener('scroll'.this.scroll);
    }
    componentWillUnmount() {
        window.removeEveneListener('scroll'.this.scroll);
    }
    @throttle(50)
    scroll(){}}Copy the code

The decorator that implements the debounce function is similar to the throttling function, but I won’t go into it again.

const debounce = (time) = > {
    let timer;
    return (target, name, descriptor) = > {
        const func = descriptor.value;
        if (typeof func === 'function') {
            descriptor.value = function(. args) {
                if(timer) clearTimeout(timer)
                timer = setTimeout(() = > {
                    fn.apply(this, args)
                }, wait)
            }
        }
    }
}
Copy the code

If you are interested in throttling and shudder functions, read this article: Function Throttling and Function Shudder

4.3 Data format verification

The class property decorator is used to verify the type of a class property.

const validate = (type) = > (target, name) = > {
    if (typeoftarget[name] ! == type) {throw new Error(`attribute ${name} must be ${type} type`)}}class Form {
    @validate('string')
    static name = 111 // Error: attribute name must be ${type} type
}
Copy the code

If you think it’s too much trouble to check each property manually, you can also write validation rules to check the entire class.

const rules = {
    name: 'string'.password: 'string'.age: 'number'
}
const validator = rules= > targetClass= > {
    return new Proxy(targetClass, {
        construct(target, args) {
            const obj = newtarget(... args);for (let [name, type] of Object.entries(rules)) {
                if (typeofobj[name] ! == type) {throw new Error(`${name} must be ${type}`)}}return obj;
        }
    })
}

@validator(rules)
class Person {
    name = 'tom'
    password = '123'
    age = '21'
}
const person = new Person();
Copy the code

4.4 the core – decorators. Js

Core-decorators is a JS library that encapsulates common decorators. It summarizes the following decorators (just a few).

  1. autobind: Automatic bindingthis, the farewell arrow function andbind
  2. readonly: sets the class attribute to read-only
  3. override: Checks that a subclass method correctly overrides a superclass method of the same name
  4. debounce: Anti-shaking function
  5. throttle: Throttling function
  6. enumerable: makes a class method enumerable
  7. nonenumerable: makes a class attribute unenumerable
  8. time: Print function execution time
  9. mixin: Mixing multiple objects into a class (not quite the same as our mixin above)

5. To summarize

Decorators, while still an unstable syntax, are widely used in frameworks such as Angular, Nestjs, and so on, much like annotations are used in Java.

Decorators have more advanced applications when combined with reflection in TypeScript, which will be covered later. PS: follow my concern public number “front-end small pavilion”, irregularly share original knowledge.

Recommended Reading:

  1. Decorator – Ruan Yifeng
  2. JS Decorator (Decorator) scene combat
  3. Explore the decorator pattern in JavaScript
  4. [King of Glory “Decorator Mode”]

18]