1. Prerequisites

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

Therefore, this article focuses only on class and method decorators

2. Project configuration

The project configuration for this article will follow the configuration I used in the previous article. Learn es6-class from Babel compilation results

3, class decoration

3.1 basis,

A decorator is a function that handles a class. The first argument to the decorator function is the target class to decorate

Let’s write a simple decorator

function Wrap(target) {
  return target;
}

@Wrap
class Polygon {}
Copy the code

run

npm run build 
Copy the code

We get the following result:

"use strict";

var _class;

function _classCallCheck(instance, Constructor) {
  if(! (instanceinstanceof Constructor)) {
    throw new TypeError("Cannot call a class as a function"); }}function Wrap(target) {
  return target;
}

var Polygon =
  Wrap(
    (_class = function Polygon() {
      _classCallCheck(this, Polygon);
    })
  ) || _class; 
Copy the code

From the results of Babel transcoding, we know that the class will not be modified if the class decorator does not return the content. And we can also know that the class decorator has already been executed if this class is referenced elsewhere. If the class decorator returns content (which is true), then our class has been overwritten. Practical example: Component decorator for vue-class-Component.

3.2. Multiple decorators decorates the same class

The subsequent operations mentioned above will not be described. Source:


function Wrap(target) {
  return target;
}

function Component(target) {
  return target;
}

function Decs(target) {
  return target;
}

@Wrap
@Component
@Decs
class Polygon {}
Copy the code

Transcoding result:


function Wrap(target) {
  return target;
}

function Component(target) {
  return target;
}

function Decs(target) {
  return target;
}

var Polygon =
  Wrap(
    (_class =
      Component(
        (_class =
          Decs(
            (_class = function Polygon() {
              _classCallCheck(this, Polygon);
            })
          ) || _class)
      ) || _class)
  ) || _class;
Copy the code

From this result, we can conclude that the decorator closer to the class executes first.

3.3 Decorators that accept parameters

When our class decorator needs to get arguments, we wrap another layer of functions around the decorator. Component () {Component ();} Component () {Component ();}


function Wrap(target) {
  return target;
}

function Component(options = {}) {
  return function (target) {
    return target;
  };
}

function Decs(target) {
  return target;
}

@Wrap
@Component
@Decs
class Polygon {}
Copy the code

Transcoding result:

function Wrap(target) {
  return target;
}

function Component() {
  var options = arguments.length > 0 && arguments[0]! = =undefined ? arguments[0] : {};
  return function (target) {
    return target;
  };
}

function Decs(target) {
  return target;
}

var Polygon =
  Wrap(
    (_class =
      Component(
        (_class =
          Decs(
            (_class = function Polygon() {
              _classCallCheck(this, Polygon);
            })
          ) || _class)
      ) || _class)
  ) || _class;
Copy the code

Function (target) {return target; function (target) {return target; }; All subsequent decorators will receive the result of this function, so our original class has been modified. If our class decorator needs to accept parameters, it must be written in the form of a function execution, otherwise it will cause a potential bug to the program, we must pay attention to this small hole. One of my favorite API design principles is disgust yourself and help others. This means that the design of the API should provide as much convenience as possible to the user, while adding a lot of extra judgment to the design of the API. This is probably the philosophy handed down from ancient times.


function anonymous(target) {
	return target;
}

function Component() {
	var isFunc = arguments.length > 0 && typeof arguments[0= = ='function';
    return isFunc ? anonymouse(arguments[0]) : anonymous;
}
Copy the code

Fault tolerance can be tolerated for a while, but not for a lifetime, such as:

 @Component(function(){})class Polygon {}
Copy the code

At this point, our application is blindfolded and buggy, so for us developers, it’s more about reading the API carefully that is the ultimate solution. The result of transcoding after properly writing decorators for acceptable parameters:

var Polygon =
  ((_dec = Component({
    age: "26".name: "hsuyang",
  })),
  Wrap(
    (_class =
      _dec(
        (_class =
          Decs(
            (_class = function Polygon() {
              _classCallCheck(this, Polygon);
            })
          ) || _class)
      ) || _class)
  ) || _class);
Copy the code

4. Method or property decorator

The method or property decorator needs to take three parameters, the first parameter is target, which is the primitive object of the class, the second parameter is the field to decorate, and the third parameter is descriptor,

Descriptor is described as follows:

interfacePropertyDescriptor { configurable? :boolean; enumerable? :boolean; value? :any; writable? :boolean; get? () :any; set? (v:any) :void;
}
Copy the code

Because decorator is in adornment attribute and method go up still have certain distinction, therefore the author disassembles here separately speak

4.1 Property decorators

Attribute decorator Babel used a method called initializer for the initialization of attributes. (I was confused at the beginning of the study. Was Ruan Yifeng wrong, but ruan Yifeng was not wrong, but his knowledge was not enough 🙄). Source:

function readonly(target, name, descriptor) {
  descriptor.writable = false;
  descriptor.value = function () {
    console.log("hsuyang");
  };
  return descriptor;
}

class Person {
  @readonly
  name1;

  @readonly
  name2 = "I love microsoft";
}
Copy the code

Transcoding result:

"use strict";

var _class, _descriptor, _descriptor2, _temp;

function _initializerDefineProperty(target, property, descriptor, context) {
  if(! descriptor)return;
  Object.defineProperty(target, property, { enumerable: descriptor.enumerable, configurable: descriptor.configurable, writable: descriptor.writable, value: descriptor.initializer ? descriptor.initializer.call(context) : void 0 });
}

function _classCallCheck(instance, Constructor) {
  if(! (instanceinstanceof Constructor)) {
    throw new TypeError("Cannot call a class as a function"); }}function _defineProperty(obj, key, value) {
  if (key in obj) {
    Object.defineProperty(obj, key, { value: value, enumerable: true.configurable: true.writable: true });
  } else {
    obj[key] = value;
  }
  return obj;
}

function _applyDecoratedDescriptor(target, property, decorators, descriptor, context) {
  var desc = {};
  Object.keys(descriptor).forEach(function (key) { desc[key] = descriptor[key]; }); desc.enumerable = !! desc.enumerable; desc.configurable = !! desc.configurable;if ("value" in desc || desc.initializer) {
    desc.writable = true;
  }
  desc = decorators
    .slice()
    .reverse()
    .reduce(function (desc, decorator) {
      return decorator(target, property, desc) || desc;
    }, desc);
  if(context && desc.initializer ! = =void 0) {
    desc.value = desc.initializer ? desc.initializer.call(context) : void 0;
    desc.initializer = undefined;
  }
  if (desc.initializer === void 0) {
    Object.defineProperty(target, property, desc);
    desc = null;
  }
  return desc;
}

function _initializerWarningHelper(descriptor, context) {
  throw new Error("Decorating class property failed. Please ensure that " + "proposal-class-properties is enabled and runs after the decorators transform.");
}

function readonly(target, name, descriptor) {
  descriptor.writable = false;

  descriptor.value = function () {
    console.log("hsuyang");
  };

  return descriptor;
}

var Person =
  ((_class =
    ((_temp = function Person() {
      _classCallCheck(this, Person);

      _initializerDefineProperty(this."name1", _descriptor, this);

      _initializerDefineProperty(this."name2", _descriptor2, this);
    }),
    _temp)),
  ((_descriptor = _applyDecoratedDescriptor(_class.prototype, "name1", [readonly], {
    configurable: true.enumerable: true.writable: true.initializer: null,
  })),
  (_descriptor2 = _applyDecoratedDescriptor(_class.prototype, "name2", [readonly], {
    configurable: true.enumerable: true.writable: true.initializer: function initializer() {
      return "I love microsoft";
    },
  }))),
  _class);

Copy the code

Code, though many, but the core is that _applyDecoratedDescriptor and _initializerDefineProperty these two methods.

function _applyDecoratedDescriptor(target, property, decorators, descriptor, context) {
  var desc = {};
  Object.keys(descriptor).forEach(function (key) { desc[key] = descriptor[key]; }); desc.enumerable = !! desc.enumerable; desc.configurable = !! desc.configurable;if ("value" in desc || desc.initializer) {
    desc.writable = true;
  }
  
  // Apply multiple decorators
  desc = decorators
    .slice()
    .reverse()
    .reduce(function (desc, decorator) {
      // If the decorator doesn't return anything, it defaults to that descriptor, because by the nature of the JS language, desc is an object, and when we pass that object inside the function, it's already been modified, so the property decorator, whether it's return Descriptor or not, The effect is the same.
      return decorator(target, property, desc) || desc;
    }, desc);
  Initializer is designed to handle assignment statements for attributes or methods in a class
  // If an assignment statement exists, the value of the attribute is equal to the result of the assignment statement
  if(context && desc.initializer ! = =void 0) {
    // Bind this to the assignment context
    desc.value = desc.initializer ? desc.initializer.call(context) : void 0;
    // After initializer is processed, it is treated as undefined. This is very important
    desc.initializer = undefined;
  }
  // If there is no assignment statement, the normal Object.defineProperty is handled. Readers who do not understand Object.defineProperty should go to MDN.
  if (desc.initializer === void 0) {
    Object.defineProperty(target, property, desc);
    desc = null;
  }
  return desc;
}

function _initializerDefineProperty(target, property, descriptor, context) {
  if(! descriptor)return;
    Object.defineProperty(target, property, { 
    enumerable: descriptor.enumerable, 
    configurable: descriptor.configurable, 
    writable: descriptor.writable, 
    If you want a decorator to assign a value to a property that is not assigned, it cannot be assigned.
    value: descriptor.initializer ? descriptor.initializer.call(context) : void 0
  });
}
Copy the code

Pay particular attention to the _initializerDefineProperty method, you want to attribute a decorator, by modifying the descriptor. The value, to attribute set value, doesn’t work. Initializer has been set to undefined, so it is useless to use initializer to change the property value.

There is another weird operation, which I have done in previous development 🤪, but it is not recommended.


function Autowired(target, name, descriptor) {
  // You want to set an attribute value for a field of the class.
  target[name] = {};
}

class Person {
  @Autowired
  name1;
}
Copy the code

Properties, both inside and outside the constructor, are defined on the instance of the class. In the assignment statement above, we actually assign to the primitive object of the class. When our class is initialized, the object has the property name1. It is just undefined, and although we define name1 on the primitive object, we automatically ignore the name1 attribute on the primitive object because we have the name1 attribute.

Therefore, we can also draw a small conclusion that trying to modify the value of a property through a property decorator is not a good way to do it, just fooling ourselves 😩.

4.2 Method decorator

Method decorations have fewer pits than attribute decorations. Source:

function log(target, name, descriptor) {
  var oldValue = descriptor.value;

  descriptor.value = function () {
    console.log(`Calling ${name} with`.arguments);
    return oldValue.apply(this.arguments);
  };

  return descriptor;
}

class Person {
  @log
  calc(){}}Copy the code

Transcoding result:

/ / redundant code is not necessary to stick out, the key is that our method decorators, not through _initializerDefineProperty, therefore, method can pass the value to modify, and add some additional operations (such as the method of actuator do, the method completes execution do).
var Person =
  ((_class = /*#__PURE__*/ (function () {
    function Person() {
      _classCallCheck(this, Person);
    }

    _createClass(Person, [
      {
        key: "calc".value: function calc() {},},]);return Person;
  })()),
  _applyDecoratedDescriptor(_class.prototype, "calc", [log], Object.getOwnPropertyDescriptor(_class.prototype, "calc"), _class.prototype),
  _class);
Copy the code

4.3 Stacking multiple attributes or methods

Source:

function log(target, name, descriptor) {
  var oldValue = descriptor.value;

  descriptor.value = function () {
    console.log(`Calling ${name} with`.arguments);
    return oldValue.apply(this.arguments);
  };

  return descriptor;
}

function log2(target, name, descriptor) {
  return descriptor;
}

class Person {
  @log
  @log2
  calc(){}}Copy the code

Transcoding result:

/ /... Omit code
// Reverse the order of decorators and execute iteratively
desc = decorators
    .slice()
    .reverse()
    .reduce(function (desc, decorator) {
      return decorator(target, property, desc) || desc;
    }, desc);
/ /... Omit code

// Read decorators in turn
_applyDecoratedDescriptor(_class.prototype, "calc", [log, log2], Object.getOwnPropertyDescriptor(_class.prototype, "calc"), _class.prototype),

Copy the code

It follows that property or method decorators are executed in the same order as class decorators, with the nearest decorator being executed first.

4.4 Attribute or method decorator with parameters

A parameterized property or method decorator is similar to a parameterized class decorator, such as:

function Prop(options = {}) {
    // Do some operations
    return function(target, prop, descriptor){
    	// Do some operations
        returndescriptor; }}Copy the code

I will not repeat it here.

5, summary

Decorators are always executed when the class is defined.

2. The class decorator accepts only one argument, the class itself, which is used to override the class if it has a true return value, or the class itself if it does not.

3. Property or method decorators take three parameters, namely target, prop, descriptor. For Babel transcoding results, the property decorator has an additional temporary property called initializer, which is used to initialize the value of the property. Property decorators can choose to return or not return descriptor effects to be consistent, because JS objects are passed into functions and the modified effects are reflected on the object, but return Descriptor is recommended.

4. Never use property decorators to overwrite property values.

5. Decorators with arguments must be written as the execution of functions.

Due to the limited level of the author, it is inevitable that there will be mistakes in the writing process. If there are any mistakes, please correct them. Your opinions will help me make better progress. If you want to reprint this article, please contact the author at [email protected]🥰