For more articles, check out the Github blog

The addition of definitions and operations on class objects (such as class and extends) in ES6 makes it less elegant to share or extend methods or behaviors across multiple classes. That’s when we need a more elegant way to get things done.

What is a decorator

Python decorator

In OOP design patterns, decorators are called decorators. OOP decorators are implemented through inheritance and composition, and Python supports OOP decorators directly from the syntax level, in addition to OOP decorators.

If you are familiar with Python, you will be familiar with it. So let’s take a look at what a decorator looks like in Python:

def decorator(f):
    print "my decorator"
    return f
@decorator
def myfunc(a):
    print "my function"
myfunc()
# my decorator
# my function
Copy the code

The @decorator here is what we call a decorator. In the above code, we use the decorator to print out a line of text for our target method before execution without making any changes to the original method. The code is basically equivalent to:

def decorator(f):
    def wrapper(a):
        print "my decorator"
        return f()
    return wrapper
def myfunc(a):
    print "my function"
myfunc = decorator(myfuc)
Copy the code

As you can see from the code, the decorator takes one parameter, the target method we decorated, processes the extended content and returns a method for later invocation, losing access to the original method object. When we apply a decorator to a function, we actually change the entry reference of the decorator method, redirecting it to the entry point of the method returned by the decorator, so that we can extend or modify the original function.

ES7 decorator

Decorators in ES7 also borrow from this syntactic sugar, but rely on ES5’s object.defineProperty method.

Object.defineProperty

The object.defineProperty () method directly defines a new property on an Object, or modifies an existing property of an Object, and returns the Object.

This method allows you to precisely add or modify attributes of an object. Normal attributes added by assignment create attributes that are displayed during attribute enumeration (for… In or object. keys methods), these values can be changed or deleted. This method allows these additional details to be changed from the default values. Attribute values added using Object.defineProperty() are immutable by default.

grammar

Object.defineProperty(obj, prop, descriptor)
Copy the code
  • obj: The object on which attributes are defined.
  • prop: The name of the property to be defined or modified.
  • descriptor: Property descriptor to be defined or modified.
  • Return value: The object passed to the function.

In ES6, because of the special nature of Symbol type, using Symbol type values for Object keys is different from regular definition or modification, and Object.defineProperty is one of the ways to define properties where the key is Symbol.

Attribute descriptor

There are two main types of property descriptors that currently exist in objects: data descriptors and access descriptors.

  • A data descriptor is a property with a value that may or may not be writable.
  • Access descriptors are properties described by getter-setter function pairs.

The descriptor must be one of these two forms; You can’t be both.

Both data descriptors and access descriptors have the following optional key values:

configurable

The property descriptor can be changed and deleted from the corresponding object only if and when the property is configured with true. The default is false.

enumerable

Enumerable defines whether an object’s property can be considered in a for… Are enumerated in the in loop and object.keys ().

An attribute can appear in an object’s enumerated property if and only if its Enumerable is true. The default is false. The data descriptor also has the following optional key values:

value

The value corresponding to this property. It can be any valid JavaScript value (numeric value, object, function, etc.). The default is undefined.

writable

Value can be changed by the assignment operator if and only if the writable of the property is true. The default is false.

The access descriptor also has the following optional key values:

get

A method that provides a getter for a property, undefined if there is no getter. The value returned by this method is used as the attribute value. The default is undefined.

set

A method that provides a setter for a property, undefined if there is no setter. This method takes a unique parameter and assigns a new value for that parameter to the property. The default is undefined.

A descriptor is considered a data descriptor if it does not have any of the keywords value,writable, GET, and set. If a descriptor has both (value or writable) and (get or set) keywords, an exception will be raised.

usage

The adornment of the class

@testable
class MyTestableClass {
  // ...
}

function testable(target) {
  target.isTestable = true;
}

MyTestableClass.isTestable // true
Copy the code

In the code above, @testable is a testable testable testable testable testable device. It modifies the behavior of the MyTestableClass class by adding the static attribute isTestable to it. Testable functions target parameters from the MyTestableClass class itself.

Basically, the decorator behaves like this.

@decorator
class A {}

/ / is equivalent to

class A {}
A = decorator(A) || A;
Copy the code

That is, a decorator is a function that processes a class. The first argument to the decorator function is the target class to decorate.

If one argument is not enough, you can wrap another function around the decorator.

function testable(isTestable) {
  return function(target) {
    target.isTestable = isTestable;
  }
}

@testable(true)
class MyTestableClass {}
MyTestableClass.isTestable // true

@testable(false)
class MyClass {}
MyClass.isTestable // false
Copy the code

The above code, the decorator testable can accept parameters, it is can modify the behavior of the decorator.

Note that the decorator changes the behavior of the class at compile time, not run time. This means that the decorator can run the code at compile time. That is, decorators are essentially functions that are executed at compile time.

The previous example added a static attribute to a class. If you want to add instance attributes, you can do so through the prototype object of the target class.

Here’s another example.

// mixins.js
export function mixins(. list) {
  return function (target) {
    Object.assign(target.prototype, ... list) } }// main.js
import { mixins } from './mixins'

const Foo = {
  foo() { console.log('foo')}}; @mixins(Foo)class MyClass {}

let obj = new MyClass();
obj.foo() // 'foo'
Copy the code

The above code adds methods on Foo objects to instances of MyClass via the decorator mixins.

Method decoration

Decorators can decorate not only classes but also class properties.

class Person {
  @readonly
  name() { return `The ${this.first} The ${this.last}`}}Copy the code

In the code above, the decorator readonly decorates the name method of the “class”.

The decorator function readonly takes a total of three arguments.

function readonly(target, name, descriptor){
  The original value of the // Descriptor object is as follows
  / / {
  // value: specifiedFunction,
  // enumerable: false,
  // configurable: true,
  // writable: true
  // };
  descriptor.writable = false;
  return descriptor;
}

readonly(Person.prototype, 'name', descriptor);
/ / similar to
Object.defineProperty(Person.prototype, 'name', descriptor);
Copy the code
  • The first argument to the decorator is the class’s prototype object (person.prototype). The decorator is intended to “decorate” the instance of the class, but the instance has not yet been generated, so it can only decorate the prototype (as opposed to the class’s decorator, in which case the target argument refers to the class itself).
  • The second parameter is the name of the property to decorate
  • The third argument is the description object of the property

In addition, the decorator (readonly) modifies the descriptor of the property, which is then used to define the property.

Decoration of function methods

Decorators can only be used for classes and methods of classes, not functions, because function promotion exists.

On the other hand, if you must decorate a function, you can execute it directly as a higher-order function.

function doSomething(name) {
  console.log('Hello, ' + name);
}

function loggingDecorator(wrapped) {
  return function() {
    console.log('Starting');
    const result = wrapped.apply(this.arguments);
    console.log('Finished');
    returnresult; }}const wrapped = loggingDecorator(doSomething);
Copy the code

core-decorators.js

Core-decorators.js is a third-party module that provides several common decorators to better understand decorators.

@autobind

The autobind decorator causes this object in the method tobind to the original object.

@readonly

The readonly decorator makes an attribute or method unwritable.

@override

The Override decorator checks that the subclass’s method properly overrides the parent’s method of the same name, and raises an error if it does not.

import { override } from 'core-decorators';

class Parent {
  speak(first, second) {}
}

class Child extends Parent {
  @override
  speak() {}
  // SyntaxError: Child#speak() does not properly override Parent#speak(first, second)
}

// or

class Child extends Parent {
  @override
  speaks() {}
  // SyntaxError: No descriptor matching Child#speaks() was found on the prototype chain.
  //
  // Did you mean "speak"?
}
Copy the code

@deprecate (alias @deprecated)

The DEPRECate or Deprecated decorator displays a warning on the console indicating that the method will be abolished.

import { deprecate } from 'core-decorators';

class Person {
  @deprecate
  facepalm() {}

  @deprecate('We stopped facepalming')
  facepalmHard() {}

  @deprecate('We stopped facepalming', { url: 'http://knowyourmeme.com/memes/facepalm' })
  facepalmHarder() {}
}

let person = new Person();

person.facepalm();
// DEPRECATION Person#facepalm: This function will be removed in future versions.

person.facepalmHard();
// DEPRECATION Person#facepalmHard: We stopped facepalming

person.facepalmHarder();
// DEPRECATION Person#facepalmHarder: We stopped facepalming
//
// See http://knowyourmeme.com/memes/facepalm for more details.
//
Copy the code

@suppressWarnings

SuppressWarnings decorator suppresses console.warn() calls caused by the Deprecated decorator. Exceptions are, however, calls made by asynchronous code.

Usage scenarios

Decorators serve as annotations

@testable
class Person {
  @readonly
  @nonenumerable
  name() { return `The ${this.first} The ${this.last}`}}Copy the code

From the above code, we can see at a glance that the Person class is testable, while the Name method is read-only and not enumerable.

The React of the connect

In real development, React and the Redux library are often written like this.

class MyReactComponent extends React.Component {}

export default connect(mapStateToProps, mapDispatchToProps)(MyReactComponent);
Copy the code

With decorators, you can rewrite the code above. decoration

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

The latter seems relatively easy to understand.

New feature alerts or permissions

When the menu is clicked, the event is blocked. If the menu has a new function update, the popup window will be displayed.

/** * @description: returns {function(*, *, *)} */
 const checkRecommandFunc = (code) = > (target, property, descriptor) => {
    let desF = descriptor.value; 
    descriptor.value = function (. args) {
      let recommandFuncModalData = SYSTEM.recommandFuncCodeMap[code];

      if (recommandFuncModalData && recommandFuncModalData.id) {
        setTimeout((a)= > {
          this.props.dispatch({type: 'global/setRecommandFuncModalData', recommandFuncModalData});
        }, 1000);
      }
      desF.apply(this, args);
    };
    return descriptor;
  };

Copy the code

loading

In the React project, we might need to animate the page as data is being requested in the background. At this point, you can use the decorator to elegantly implement the function.

@autobind
@loadingWrap(true)
async handleSelect(params) {
  await this.props.dispatch({
    type: 'product_list/setQuerypParams'.querypParams: params
  });
}
Copy the code

The loadingWrap function looks like this:

export function loadingWrap(needHide) {

  const defaultLoading = (
    <div className="toast-loading">
      <Loading className="loading-icon"/>
      <div>Loading in...</div>
    </div>); return function (target, property, descriptor) { const raw = descriptor.value; descriptor.value = function (... args) { Toast.info(text || defaultLoading, 0, null, true); const res = raw.apply(this, args); if (needHide) { if (get('finally')(res)) { res.finally(() => { Toast.hide(); }); } else { Toast.hide(); }}}; return descriptor; }; }Copy the code

Q: If we do not want loading every time we request data, but require loading only when the background request time is more than 300ms, what should we change?

reference

  • Object.defineProperty()
  • JavaScript Decorators: What They Are and When to Use Them
  • ECMAScript introduction to 6