Decorator: From Principle to Practice

Keywords: Javascript Attribute React

Preface

Original Link: Nealyang/personalBlog

ES6 doesn't have to be introduced much anymore. Before ES6, decorators probably weren't that important because you just needed to add a wrapper layer, but now, with the advent of grammatical sugar classes, the code gets complicated and difficult when we want to share or extend methods between classesTo maintain, and that's where our Decorator comes in.

Object.defineProperty

Simply stated about Object.defineProperty is that it accurately adds and modifies properties of objects

grammar

Object.defineProperty(obj,prop,descriptor)

  • ojb: Object on which to define properties
  • prop: Name of the property to be defined or modified
  • Descriptor: Attribute descriptor to be defined or modified

This method returns the object passed to the function

In ES6, because of the special nature of the Symbol type, using a value of the Symbol type to make the key of an object is different from the general definition or modification, and Object.defineProperty is one of the ways to define a key as a property of the Symbol.

Common attributes added by assignment operations are enumerable and can be presented during the enumeration of attributes (for...in or Object.keys methods), and their values can be changed or deleted.This method allows you to modify the default additional options (or configurations).By default, attribute values added using Object.defineProperty() are not modifiable

Generic Descriptor

There are two main forms of attribute descriptors currently present in objects: data descriptors and access descriptors.A data descriptor is an attribute with a value that may or may not be writable.Access descriptors are properties described by getter-setter functions.Descriptors must be one of these two forms; they cannot be both.

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

configurable

The property descriptor can only be changed if and only if the property's configurable is true, and the property can also be deleted from the corresponding object.Default to false

enumerable

This property can only appear in an object's enumeration property if and only if its enumerable is true.Default to false.

Data descriptors also have the following optional key values:

value

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

writable

value can only be changed by the assignment operator if and only if the property's writable is true.Default to false

Access descriptors also have the following optional key values:

get

A method that provides a getter to an attribute, or undefined if no getter exists.When the property is accessed, the method is executed, and no parameters are passed in when the method is executed, but a this object is passed in (where this is not necessarily the object defining the property due to inheritance).Default is undefined.

set

A method that provides a setter to an attribute, or undefined if no setter exists.This method is triggered when the attribute value is modified.The method will accept the unique parameter, which is the new parameter value for the attribute.Default is undefined.

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

For more usage examples and descriptions, see: MDN

Decorator Mode

Before we look at Decorator, let's look at the use of Decorator mode. We all know that Decorator mode can add blame to an object while the program is running, without changing the object itself.The feature is to add additional functions of responsibility without affecting the characteristics of previous objects.

like...this:

This is a relatively simple passage, just look at the code:

let Monkey = function () {}
Monkey.prototype.say = function () {
  console.log('I'm just a wild monkey right now');
}
let TensionMonkey = function (monkey) {
  this.monkey = monkey;
}
TensionMonkey.prototype.say = function () {
  this.monkey.say();
  console.log('With the hoop curse, I'll forget the world's troubles!');
}
let monkey = new TensionMonkey(new Monkey());
monkey.say();

Execution results:

Decorator

Decorator is actually a grammatical sugar. Behind it is the use of es5's Object.defineProperty(target,name,descriptor) to understand Object.defineProperty. Please move on This link: MDN Document

The general principle behind it is as follows:

class Monkey{
  say(){
    console.log('At the moment, I'm just a wild monkey');
  }
}

Execute the code above, roughly as follows:

Object.defineProperty(Monkey.prototype,'say',{
  value:function(){console.log('At the moment, I'm just a wild monkey')},
  enumerable:false,
  configurable:true,
  writable:true
})

If we decorate him with ornaments

class Monkey{
@readonly
say(){console.log('Now I am read-only')}
}

The properties of this decorator execute the following code before Object.defineProperty registers the say property for Monkey.prototype:

let descriptor = {
  value:specifiedFunction,
  enumerable:false,
  configurable:true,
  writeable:true
};

descriptor = readonly(Monkey.prototype,'say',descriptor)||descriptor;
Object.defineProperty(Monkey.prototype,'say',descriptor);

From the pseudo code above, we can see that Decorator just executed a decorative function that belongs to a class intercepting Object.defineProperty before Object.defineProperty registered the property for Monkey.prototype.So it has the same formal parameters as Object.defineProperty:

  • obj: the target object of the action
  • prop:Property name of the role
  • descriptor: descriptor for this property

Here's a look at simple use

Use in class

  • Create a new class that inherits from the original class and add attributes
@name
class Person{
  sayHello(){
    console.log(`hello ,my name is ${this.name}`)
  }
}

function name(constructor) {  
  return class extends constructor{
    name="Nealyang"
  }
}

new Person().sayHello()
//hello ,my name is Nealyang
  • Modify for current class (similar to mixin)
@name
@seal
class Person {
  sayHello() {
    console.log(`hello ,my name is ${this.name}`)
  }
}

function name(constructor) {
  Object.defineProperty(constructor.prototype,'name',{
    value:'One Side'
  })
}
new Person().sayHello()

//If you modify an attribute

function seal(constructor) {
  let descriptor = Object.getOwnPropertyDescriptor(constructor.prototype, 'sayHello')
  Object.defineProperty(constructor.prototype, 'sayHello', {
    ...descriptor,
    writable: false
  })
}

new Person().sayHello = 1;// Cannot assign to read only property 'sayHello' of object '#<Person>'

When it comes to mixins, let me simulate a mixin

class A {
  run() {
    console.log('I can run!')
  }
}

class B {
  jump() {
    console.log('I can jump!')
  }
}

@mixin(A, B)
class C {}

function mixin(...args) {
  return function (constructor) {
    for (const arg of args) {
      for (let key of Object.getOwnPropertyNames(arg.prototype)) {
        if (key === 'constructor') continue;
        Object.defineProperty(constructor.prototype, key, Object.getOwnPropertyDescriptor(arg.prototype, key));
      }
    }
  }
}

let c = new C();
c.jump();
c.run();
// I can jump!
// I can run!

So far we seem to have written a lot of code, right.This one, for the sake of investing in Decorator completely, here...Just start.

Use in class members

This type of decorator should be written in the way we know it best, accepting three parameters:

  • If the decorator is mounted on a static member, the constructor is returned, and if it is mounted on an instance member, the prototype of the class is returned
  • Member name mounted by decorator
  • Return value of Object.getOwnPropertyDescriptor

First, let's clarify the difference between static and instance members

class Model{
  //Instance members
  method1(){}
  method2 = ()=>{}
  
  // Static members
  static method3(){}
  static method4 = ()=>{}
}

Method1 and method2 are instance members, but method1 exists on the prototype, and method2 only exists after the object is instantiated.

method3 and method4 are static members. The difference between them is whether descriptor settings can be enumerated. We can see through babel transcoding:

The above code is messy and can be easily understood as:

function Model () {
  // Members are only assigned when instantiated
  this.method2 = function () {}
}

// Members are defined on the prototype chain
Object.defineProperty(Model.prototype, 'method1', {
  value: function () {}, 
  writable: true, 
  enumerable: false,  // Settings cannot be enumerated
  configurable: true
})

// Members are defined on the constructor and are default enumerable
Model.method4 = function () {}

// Members are defined on constructors
Object.defineProperty(Model, 'method3', {
  value: function () {}, 
  writable: true, 
  enumerable: false,  // Settings cannot be enumerated
  configurable: true
})

You can see that only method2 is assigned when instantiated, and a nonexistent attribute does not have a descriptor, so that's why the third parameter is not passed for Property Decorator. There is no reasonable explanation as to why static members do not pass descriptors, butIf it is explicitly intended to be used, it can be obtained manually.

As in the example above, once we've added decorators for all four members, the first parameter for method1 and method2 is Model.prototype, while the first parameter for method3 and method4 is Model.

class Model {
  // Instance members
  @instance
  method1 () {}
  @instance
  method2 = () => {}

  // Static members
  @static
  static method3 () {}
  @static
  static method4 = () => {}
}

function instance(target) {
  console.log(target.constructor === Model)
}

function static(target) {
  console.log(target === Model)
}

Use of function, accessor, property decorators

  • The return value of the function decorator defaults to the presence of the value descriptor as an attribute and is ignored if returned to undefined
class Model {
  @log1
  getData1() {}
  @log2
  getData2() {}
}

// Scenario one, returning a new value descriptor
function log1(tag, name, descriptor) {
  return {
    ...descriptor,
    value(...args) {
      let start = new Date().valueOf()
      try {
        return descriptor.value.apply(this, args)
      } finally {
        let end = new Date().valueOf()
        console.log(`start: ${start} end: ${end} consume: ${end - start}`)
      }
    }
  }
}

// Option 2. Modify existing descriptors
function log2(tag, name, descriptor) {
  let func = descriptor.value // Get the previous function first

  // Modify the corresponding value
  descriptor.value = function (...args) {
    let start = new Date().valueOf()
    try {
      return func.apply(this, args)
    } finally {
      let end = new Date().valueOf()
      console.log(`start: ${start} end: ${end} consume: ${end - start}`)
    }
  }
}
  • The accessor's Decorator is the get set prefix function, which controls the assignment and evaluation of attributes. It is no different from the function decorator in use.
class Modal {
  _name = 'Niko'

  @prefix
  get name() { return this._name }
}

function prefix(target, name, descriptor) {
  return {
    ...descriptor,
    get () {
      return `wrap_${this._name}`
    }
  }
}

console.log(new Modal().name) // wrap_Niko
  • There is no descriptor return for an attribute decorator, and the return value of the decorator function is ignored. If we need to modify a static attribute, we need to get the descriptor ourselves
  class Modal {
    @prefix
    static name1 = 'Niko'
  }
  
  function prefix(target, name) {
    let descriptor = Object.getOwnPropertyDescriptor(target, name)
  
    Object.defineProperty(target, name, {
      ...descriptor,
      value: `wrap_${descriptor.value}`
    })
  }
  
  console.log(Modal.name1) // wrap_Niko

There are no directly modified schemes for the properties of an instance, but we can combine some other ornaments to save the nation.

For example, we have a class that will pass in name and age as initialization parameters, and then we will set the corresponding formatting checks for these two parameters

  const validateConf = {} // Store verification information
  
  @validator
  class Person {
    @validate('string')
    name
    @validate('number')
    age
  
    constructor(name, age) {
      this.name = name
      this.age = age
    }
  }
  
  function validator(constructor) {
    return class extends constructor {
      constructor(...args) {
        super(...args)
  
        // Traverse all the check information for validation
        for (let [key, type] of Object.entries(validateConf)) {
          if (typeof this[key] !== type) throw new Error(`${key} must be ${type}`)
        }
      }
    }
  }
  
  function validate(type) {
    return function (target, name, descriptor) {
      // Pass in the property name and type to be checked to the global object
      validateConf[name] = type
    }
  }
  
  new Person('Niko', '18')  // throw new error: [age must be number]
  

Function parameter decorator

  const parseConf = {}
  class Modal {
    @parseFunc
    addOne(@parse('number') num) {
      return num + 1
    }
  }
  
  // Perform formatting operations before function calls
  function parseFunc (target, name, descriptor) {
    return {
      ...descriptor,
      value (...arg) {
        // Get Formatting Configuration
        for (let [index, type] of parseConf) {
          switch (type) {
            case 'number':  arg[index] = Number(arg[index])             break
            case 'string':  arg[index] = String(arg[index])             break
            case 'boolean': arg[index] = String(arg[index]) === 'true'  break
          }
  
          return descriptor.value.apply(this, arg)
        }
      }
    }
  }
  
  // Add corresponding formatting information to global objects
  function parse(type) {
    return function (target, name, index) {
      parseConf[index] = type
    }
  }
  
  console.log(new Modal().addOne('10')) // 11
  

Decorator use case

log

Add the log function to a method and check the input parameters

    let log = type => {
      return (target,name,decorator) => {
        const method = decorator.value;
        console.log(method);

        decorator.value = (...args) => {
          console.info(`${type} Underway: ${name}(${args}) = ?`);
          let result;
          try{
            result = method.apply(target,args);
            console.info(`(${type}) Success : ${name}(${args}) => ${result}`);
          }catch(err){
            console.error(`(${type}) fail: ${name}(${args}) => ${err}`);
          }
          return result;
        }
      }
    }

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

    const math = new Math();

    // (add) success: add (2,4) => 6
    math.add(2, 4);

time

Time for statistical method execution:

function time(prefix) {
  let count = 0;
  return function handleDescriptor(target, key, descriptor) {

    const fn = descriptor.value;

    if (prefix == null) {
      prefix = `${target.constructor.name}.${key}`;
    }

    if (typeof fn !== 'function') {
      throw new SyntaxError(`@time can only be used on functions, not: ${fn}`);
    }

    return {
      ...descriptor,
      value() {
        const label = `${prefix}-${count}`;
        count++;
        console.time(label);

        try {
          return fn.apply(this, arguments);
        } finally {
          console.timeEnd(label);
        }
      }
    }
  }
}

debounce

Anti-shake the method being executed

  class Toggle extends React.Component {
  
    @debounce(500, true)
    handleClick() {
      console.log('toggle')
    }
  
    render() {
      return (
        <button onClick={this.handleClick}>
          button
        </button>
      );
    }
  }
  
  function _debounce(func, wait, immediate) {
  
      var timeout;
  
      return function () {
          var context = this;
          var args = arguments;
  
          if (timeout) clearTimeout(timeout);
          if (immediate) {
              var callNow = !timeout;
              timeout = setTimeout(function(){
                  timeout = null;
              }, wait)
              if (callNow) func.apply(context, args)
          }
          else {
              timeout = setTimeout(function(){
                  func.apply(context, args)
              }, wait);
          }
      }
  }
  
  function debounce(wait, immediate) {
    return function handleDescriptor(target, key, descriptor) {
      const callback = descriptor.value;
  
      if (typeof callback !== 'function') {
        throw new SyntaxError('Only functions can be debounced');
      }
  
      var fn = _debounce(callback, wait, immediate)
  
      return {
        ...descriptor,
        value() {
          fn()
        }
      };
    }
  }

More examples of core-decorators come later
Nealyang/PersonalBlog In addition to the notes.

Reference resources

Exchange of learning

Focus on Public Number: [Selected Front End of Full Stack] Get good text recommendations every day.

Reply [1] from the public number, join the front-end learning group of the whole stack and communicate with each other.

Posted by joquius on Tue, 24 Sep 2019 19:46:59 -0700