Decorator is a descriptive word that begins with an @ sign. In English, the verb “decorator” means “decorate”. The root dek(pronounced dec) means “to accept” in proto-Indo-European languages. That is, something that is old takes on something new (and becomes better). From another perspective, decorators work primarily on the outside of the object being decorated, rather than intruding on what happens inside. Decorator pattern is also a development pattern, although its status is weaker than MVC, IoC, etc., but it is an excellent pattern.

JavaScript decorators may be borrowed from Python or Java. The obvious difference is that most languages’ decorators must be line by line, whereas JS ‘decorators can be on a single line.

Why decorators exist

A good programmer is a lazy programmer.

Here’s an example: I enter the headquarters building with my employee card. Because each employee belongs to a different department and level, they are not allowed to enter any room in the building. Each room has a door; Then, the company needs to assign at least one person in each office to the job of validating visitors:

  1. Register visitors first
  2. Verify that they have access, and ask them to leave if they do not
  3. Record the time of departure

Another option is to install electronic door locks, which simply transmit the employee card information to the machine room for verification by a specific program.

The former is called stupid mode, and the code looks like this:

function A101(who){
  record(who,new Date(),'enter');
  if(! permission(who)) { record(who,new Date(),'no permission')
    return void;
  }
  // Continue to execute
  doSomeWork();
  record(who,new Date(),'leave')}function A102(who){
record(who,new Date(),'enter');
  if(! permission(who)) { record(who,new Date(),'no permission')
    return void;
  }
  // Continue to execute
  doSomeWork();
  record(who,new Date(),'leave')}// ... 
Copy the code

Experienced people must have thought of the first time, those repeated statements into a method, and unified call. Yes, it solves most problems, but it’s not “elegant.” And there’s another question: wouldn’t it be “sick” if there were too many “rooms”, or if only the odd number of rooms in the building had to be verified without even numbers? Using decorator mode, the code would look something like this:

@verify(who)
class Building {
  @verify(who)
  A101(){/ *... * /}
  @verify(who)
  A102(){/ *... * /}
  / /...
}
Copy the code

Verify is a decorator for validation, which is essentially a set of functions.

JavaScript decorator

As in the previous example, the decorator itself is a function that is executed before the object being decorated is executed.

In JavaScript, the types of decorators are:

  • class
  • Access methods (get and set of attributes)
  • field
  • methods
  • parameter

Because the decorator concept is currently in the proposal stage and is not an officially available JS feature, you will have to use a translator tool such as Babel or TypeScript to compile your JS code before it can be executed. We need to set up the environment and set up some parameters. (The following procedure assumes that the NodeJS development environment and package management tools have been correctly installed)

cd project && npm init
npm i -D @babel/cli @babel/core @babel/plugin-proposal-class-properties @babel/plugin-proposal-decorators @babel/preset-env babel-plugin-parameter-decorator
Copy the code

Create a. Babelrc configuration file as follows:

{
  "presets": ["@babel/preset-env"]."plugins": [["@babel/plugin-proposal-decorators", { "legacy": true }],
    ["@babel/plugin-proposal-class-properties", { "loose": true}]."babel-plugin-parameter-decorator"]}Copy the code

Using the following conversion command, we can get the ES5 conversion program:

npx babel source.js –out-file target.js

Class decorator

Create a JS program that uses a decorator and uses it

@classDecorator
class Building {
  constructor() {
    this.name = "company"; }}const building = new Building();

function classDecorator(target) {
  console.log("target", target);
}
Copy the code

The above is a very simple decorator program, which we use Babel to “translate” into an ES5 program, and then beautify it to get the following program

"use strict";

var _class;

function _classCallCheck(instance, Constructor) {
  if(! (instanceinstanceof Constructor)) {
    throw new TypeError("Cannot call a class as a function"); }}var Building =
  classDecorator(
    (_class = function Building() {
      _classCallCheck(this, Building);

      this.name = "company";
    })
  ) || _class;

var building = new Building();

function classDecorator(target) {
  console.log("target", target);
}
Copy the code

Line 12 calls the function-shaped decorator during class generation and feeds the constructor (the class itself) into it. It also reveals why the first argument to the decorator is the class constructor.

Method decorator

A few changes to the code, still keeping it as simple as possible:

class Building {
  constructor() {
    this.name = "company";
  }
  @methodDecorator
  openDoor() {
    console.log("The door being open"); }}const building = new Building();

function methodDecorator(target, property, descriptor) {
  console.log("target", target);
  if (property) {
    console.log("property", property);
  }
  if (descriptor) {
    console.log("descriptor", descriptor);
  }
  console.log("=====end of decorator=========");
}
Copy the code

Then you convert the code, and you see that the amount of code is suddenly much larger this time. Exclude the _classCallCheck, _defineProperties, and _createClass functions, and focus on the _applyDecoratedDescriptor function:

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;
}
Copy the code

It executes the function after generating the constructor, noting that the decorator function is passed as an array of arguments. Then go to lines 17-22 of the above code and apply the decorators one by one, with the call to the decorator on line 21. It sends three arguments, the target being the class itself. Property is the method name (or property name), desc is the descriptor that may have been handled by the previous decorator, if it’s the first loop or if there’s only one decorator, then the method or property descriptor itself.

Accessor decoration

In the JS class definition, the get and set keywords are supported to set a field to read and write logic, and the decorator also supports the operation of such methods.

class Building {
  constructor() {
    this.name = "company";
  }
  @propertyDecorator
  get roomNumber() {
    return this._roomNumber;
  }

  _roomNumber = "";
  openDoor() {
    console.log("The door being open"); }}Copy the code

As interested readers may have noticed, the accessor decorates the code very closely to the method decorates code above. The property GET and set methods are themselves a special form of a method. So the code between them is very close.

Property decorator

Continue modifying the source code:

class Building {
  constructor() {
    this.name = "company";
  }
  @propertyDecorator
  roomNumber = "";
}

const building = new Building();

function propertyDecorator(target, property, descriptor) {
  console.log("target", target);
  if (property) {
    console.log("property", property);
  }
  if (descriptor) {
    console.log("descriptor", descriptor);
  }
  console.log("=====end of decorator=========");
}

Copy the code

The converted code is still very close to the code for the attributes and accessors described above. But besides _applyDecoratedDescriptor, still have one more _initializerDefineProperty function. This function binds the declared various fields to the object when the constructor is generated.

Parameter decorator

The parameter decorator is used in a slightly different position than the previous centralized decorator; it is used within the line.

class Building {
  constructor() {
    this.name = "company";
  }
  openDoor(@parameterDecorator num, @parameterDecorator zoz) {
    console.log(`${num} door being open`); }}const building = new Building();

function parameterDecorator(target, property, key) {
  console.log("target", target);
  if (property) {
    console.log("property", property);
  }
  if (key) {
    console.log("key", key);
  }
  console.log("=====end of decorator=========");
}
Copy the code

Babel does not generate a specific function to operate on it. Instead, it calls the developer’s own decorator function directly after creating the class (constructor) and its associated properties and methods:

var Building = /*#__PURE__*/function () {
  function Building() {
    _classCallCheck(this, Building);

    this.name = "company";
  }

  _createClass(Building, [{
    key: "openDoor".value: function openDoor(num, zoz) {
      console.log("".concat(num, " door being open")); }}]); parameterDecorator(Building.prototype,"openDoor".1);
  parameterDecorator(Building.prototype, "openDoor".0);
  returnBuilding; } ();Copy the code

Decorator application

Use the parameter — closure

In all of these cases, the decorator itself does not use any parameters. However, in practical application, there is often a need for specific parameter requirements. Let’s go back to the example at the beginning of verify(who), where we need to pass in an identity variable. What to do? Let’s change the class decorator code a little:

const who = "Django";
@classDecorator(who)
class Building {
  constructor() {
    this.name = "company"; }}Copy the code

After conversion, you get

// ...
var who = "Django";
var Building =
  ((_dec = classDecorator(who)),
  _dec(
    (_class = function Building() {
      _classCallCheck(this, Building);

      this.name = "company";
    })
  ) || _class);
// ...
Copy the code

Notice in line 4 and 5 that it executes the decorator and then feeds the class (constructor) with the return value. Instead, we should write the constructor as follows:

function classDecorator(people) {
  console.log(`hi~ ${people}`);
  return function (target) {
    console.log("target", target);
  };
}
Copy the code

The same is true for method, accessor, property, and parameter decorators.

Decorator wrapping method

At this point, we can combine decorator parameters with the target object to do some logical class operations. So back to the example at the beginning of this article: visitor permissions are verified in the requirement, then recorded, and then recorded again when the visitor leaves. At this point, you need to oversee the entire process through which object methods are called.

Notice that descriptor for the method decorator, we can use this object to override this method.

class Building {
  constructor() {
    this.name = "company";
  }

  @methodDecorator("Gate")
  openDoor(firstName, lastName) {
    return `The door will be open, when ${firstName} ${lastName} is walking into the The ${this.name}. `; }}let building = new Building();
console.log(building.openDoor("django"."xiang"));

function methodDecorator(door) {
  return function (target, property, descriptor) {
    let fn = descriptor.value;
    descriptor.value = function (. args) {
      let [firstName, lastName] = args;
      console.log(`log: ${firstName}, who are comming.`);
      // verify(firstName,lastName)
      let result = Reflect.apply(fn, this, [firstName, lastName]);
      console.log(`log: ${result}`);
      console.log(`log: ${firstName}, who are leaving.`);
      return result;
    };
    return descriptor;
  };
}
Copy the code

At line 17, the original method is provisionally stored; Lines 18 define a new method, and lines 20 to 25 record, validate, and record the action away.

log: Django, who are comming.
log: The door will be open, when Django Xiang is walking in to the company.
log: Django, who are leaving.
The door will be open, when Django Xiang is walking in to the company
Copy the code

Decoration order

By reading the converted code, we know that the time for the decorator to work is to finish decorating the function in the generation before the class is instantiated. So, what happens if multiple decorators of different types act simultaneously? Let’s put it all together:

const who = "Django";
@classDecorator(who)
class Building {
  constructor() {
    this.name = "company";
  }

  @propertyDecorator
  roomNumber = "";

  @methodDecorator
  openDoor(@parameterDecorator num) {
    console.log(`${num} door being open`);
  }

  @accessorDecorator
  get roomNumber() {
    return this._roomNumber; }}const building = new Building();

function classDecorator(people) {
  console.log(`class decorator`);
  return function (target) {
    console.log("target", target);
  };
}

function methodDecorator(target, property, descriptor) {
  console.log("method decorator");
}

function accessorDecorator(target, property, descriptor) {
  console.log("accessor decorator");
}

function propertyDecorator(target, property, descriptor) {
  console.log("property decoator");
}

function parameterDecorator(target, property, key) {
  console.log("parameter decorator");
}
Copy the code

class decorator parameter decorator property decoator method decorator accessor decorator

You can also read the converted source code to get the execution order:

  1. Class decorator (in the outermost layer)
  2. Parameter decorator (in the innermost layer of the generation constructor)
  3. In the order of occurrence: properties, methods, and accessors

conclusion

Decorators are an elegant development model that greatly facilitates the coding process and improves the readability of the code. When developing with decorators, it is important to understand how they work.

Also, give me a “like” for seeing this…