Decorator

[Explanation] The Decorator proposal has undergone major revisions and has not yet been finalized. I don't know if the grammar will change. The following content is completely based on the previous proposal and is a bit outdated. After waiting for the verdict, it needs to be completely rewritten.

Decorator is a class-related syntax used to annotate or modify classes and class methods. Many object-oriented languages ​​have this feature, and there is currently a proposal to introduce it into ECMAScript.

Decorator is a kind of function, written as @ + function name. It can be placed before the definition of classes and class methods.

@frozen
class Foo {
  @configurable(false)
  @enumerable(true)
  method() {}

  @throttle(500)
  expensiveMethod() {}
}

The above code uses a total of four decorators, one is used in the class itself, and the other three are used in the class method. They not only increase the readability of the code, clearly express the intent, but also provide a convenient means to add or modify the functionality of the class.

Class decoration

Decorators can be used to decorate entire classes.

@testable
class MyTestableClass {
  // ...
}

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

MyTestableClass.isTestable; // true

In the above code, @testable is a decorator. It modifies the behavior of the class MyTestableClass and adds the static property isTestable to it. The parameter target of the testable function is the MyTestableClass class itself.

Basically, the behavior of the decorator is as follows.

@decorator
class A {}

// Equivalent to

class A {}
A = decorator(A) || A;

In other words, the decorator is a function that processes the class. The first parameter of the decorator function is the target class to be decorated.

function testable(target) {
  // ...
}

In the above code, the parameter target of the testable function is the class that will be decorated.

If you feel that one parameter is not enough, you can encapsulate another layer of function outside 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

In the above code, the decorator testable can accept parameters, which is equivalent to modifying the behavior of the decorator.

Note that the decorator changes the behavior of the class when the code is compiled, not at runtime. This means that the decorator can run the code during the compilation phase. In other words, the decorator is essentially a function executed at compile time.

The previous example is to add a static property to the class. If you want to add an instance property, you can operate it through the prototype object of the target class.

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

@testable
class MyTestableClass {}

let obj = new MyTestableClass();
obj.isTestable; // true

In the above code, the decorator function testable adds attributes to the prototype object of the target class, so it can be called on the instance.

Here is 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'

The above code adds the method of the Foo object to the instance of MyClass through the decorator mixins. You can simulate this function with Object.assign().

const Foo = {
  foo() {
    console.log("foo");
  },
};

class MyClass {}

Object.assign(MyClass.prototype, Foo);

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

In actual development, when React is used in combination with Redux libraries, it is often necessary to write the following.

class MyReactComponent extends React.Component {}

export default connect(mapStateToProps, mapDispatchToProps)(MyReactComponent);

With the decorator, the above code can be rewritten.

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

Relatively speaking, the latter way of writing seems easier to understand.

Method decoration

Decorators can not only decorate the class, but also decorate the attributes of the class.

class Person {
  @readonly
  name() {
    return `${this.first} ${this.last}`;
  }
}

In the above code, the decorator readonly is used to decorate the name method of the "class".

The decorator function readonly can accept three parameters in total.

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);

The first parameter of the decorator is the prototype object of the class. The above example is Person.prototype. The original intention of the decorator is to "decorate" the instance of the class. Different from the decoration of the class, in that case the target parameter refers to the class itself); the second parameter is the name of the attribute to be decorated, and the third parameter is the description object of the attribute.

In addition, the above code shows that the decorator (readonly) will modify the description object of the attribute (descriptor), and then the modified description object will be used to define the attribute.

The following is another example, modifying the enumerable property of the property description object so that the property cannot be traversed.

class Person {
  @nonenumerable
  get kidCount() {
    return this.children.length;
  }
}

function nonenumerable(target, name, descriptor) {
  descriptor.enumerable = false;
  return descriptor;
}

The following @log decorator can play a role in outputting logs.

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

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;
}

const math = new Math();

// passed parameters should get logged now
math.add(2, 4);

In the above code, the role of the @log decorator is to execute console.log once before performing the original operation, so as to achieve the purpose of outputting the log.

The decorator serves as a comment.

@testable
class Person {
  @readonly
  @nonenumerable
  name() {
    return `${this.first} ${this.last}`;
  }
}

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

Below is the component written using Decorator, which looks clear at a glance.

@Component({
  tag: "my-component",
  styleUrl: "my-component.scss",
})
export class MyComponent {
  @Prop() first: string;
  @Prop() last: string;
  @State() isVisible: boolean = true;

  render() {
    return (
      <p>
        Hello, my name is {this.first} {this.last}
      </p>
    );
  }
}

If there are multiple decorators for the same method, it will be like peeling an onion, first entering from the outside to the inside, and then executing from the inside to the outside.

function dec(id) {
  console.log("evaluated", id);
  return (target, property, descriptor) => console.log("executed", id);
}

class Example {
  @dec(1)
  @dec(2)
  method() {}
}
// evaluated 1
// evaluated 2
// executed 2
// executed 1

In the above code, the outer decorator @dec(1) enters first, but the inner decorator @dec(2) executes first.

In addition to comments, decorators can also be used for type checking. So, for classes, this feature is quite useful. In the long run, it will be an important tool for static analysis of JavaScript code.

Why can't decorators be used for functions?

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

var counter = 0;

var add = function () {
  counter++;
};

@add
function foo() {
}

The intent of the above code is that counter is equal to 1 after execution, but in fact the result is that counter is equal to 0. Because of the function promotion, the actual code executed is as follows.

var counter;
var add;

@add
function foo() {
}

counter = 0;

add = function () {
  counter++;
};

Here is another example.

var readOnly = require("some-decorator");

@readOnly
function foo() {
}

The above code is also problematic, because the actual execution is as follows.

var readOnly;

@readOnly
function foo() {
}

readOnly = require("some-decorator");

In short, due to the existence of function promotion, decorators cannot be used in functions. The class will not be promoted, so there is no problem in this regard.

On the other hand, if you must decorate the function, you can directly execute it in the form of 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");
    return result;
  };
}

const wrapped = loggingDecorator(doSomething);

core-decorators.js

core-decorators.js is a third-party module that provides several common decorators, through which you can better understand decorators.

(1)@autobind

The autobind decorator makes the this object in the method bind the original object.

import { autobind } from "core-decorators";

class Person {
  @autobind
  getPerson() {
    return this;
  }
}

let person = new Person();
let getPerson = person.getPerson;

getPerson() === person;
// true

(2)@readonly

The readonly decorator makes properties or methods unwritable.

import { readonly } from "core-decorators";

class Meal {
  @readonly
  entree = "steak";
}

var dinner = new Meal();
dinner.entree = "salmon";
// Cannot assign to read only property'entree' of [object Object]

(3) @override

The override decorator checks whether the method of the subclass correctly covers the method of the parent class with the same name. If it is not correct, an error will be reported.

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"?
}

(4) @deprecate (alias @deprecated)

The deprecate or deprecated decorator displays a warning on the console indicating that the method will be deprecated.

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.
//

** (5) @suppressWarnings**

The suppressWarnings decorator suppresses console.warn() calls caused by the deprecated decorator. However, except for calls made by asynchronous code.

import { suppressWarnings } from "core-decorators";

class Person {
  @deprecated
  facepalm() {}

  @suppressWarnings
  facepalmWithoutWarning() {
    this.facepalm();
  }
}

let person = new Person();

person.facepalmWithoutWarning();
// no warning is logged

Use decorators to implement automatic publishing events

We can use decorators to automatically emit an event when an object's method is called.

const postal = require("postal/lib/postal.lodash");

export default function publish(topic, channel) {
  const channelName = channel || "/";
  const msgChannel = postal.channel(channelName);
  msgChannel.subscribe(topic, (v) => {
    console.log("Channel:", channelName);
    console.log("Event:", topic);
    console.log("Data:", v);
  });

  return function (target, name, descriptor) {
    const fn = descriptor.value;

    descriptor.value = function () {
      let value = fn.apply(this, arguments);
      msgChannel.publish(topic, value);
    };
  };
}

The above code defines a decorator named publish, which rewrites descriptor.value so that when the original method is called, it will automatically emit an event. The event "publish/subscribe" library it uses is Postal.js.

Its usage is as follows.

// index.js
import publish from "./publish";

class FooComponent {
  @publish("foo.some.message", "component")
  someMethod() {
    return { my: "data" };
  }
  @publish("foo.some.other")
  anotherMethod() {
    // ...
  }
}

let foo = new FooComponent();

foo.someMethod();
foo.anotherMethod();

In the future, whenever someMethod or anotherMethod is called, an event will be automatically emitted.

$ bash-node index.js
Channel: component
Event: foo.some.message
Data: {my:'data'}

Channel: /
Event: foo.some.other
Data: undefined

Mixin

On the basis of the decorator, the Mixin mode can be implemented. The so-called Mixin mode is an alternative to object inheritance. The Chinese translation is "mix in" (mix in), which means a method of mixing one object into another.

Please see the example below.

const Foo = {
  foo() {
    console.log("foo");
  },
};

class MyClass {}

Object.assign(MyClass.prototype, Foo);

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

In the above code, the object Foo has a foo method. Through the Object.assign method, the foo method can be "mixed" into the MyClass class, resulting in the MyClass instance obj object having foo method. This is a simple implementation of the "mix-in" mode.

Next, we deploy a general script mixins.js to write Mixin as a decorator.

export function mixins(...list) {
  return function (target) {
    Object.assign(target.prototype, ...list);
  };
}

Then, you can use the above decorator to "mix in" various methods for the class.

import { mixins } from "./mixins";

const Foo = {
  foo() {
    console.log("foo");
  },
};

@mixins(Foo)
class MyClass {}

let obj = new MyClass();
obj.foo(); // "foo"

Through the decorator of mixins, the foo method of "mixing" the Foo object on the MyClass class is realized.

However, the above method will rewrite the prototype object of the MyClass class. If you don't like this, you can also implement Mixin through class inheritance.

class MyClass extends MyBaseClass {
  /* ... */
}

In the above code, MyClass inherits MyBaseClass. If we want to "mix in" a foo method in MyClass, one way is to insert a mixin class between MyClass and MyBaseClass. This class has a foo method and inherits from MyBaseClass All methods, and then MyClass inherits this class.

let MyMixin = (superclass) =>
  class extends superclass {
    foo() {
      console.log("foo from MyMixin");
    }
  };

In the above code, MyMixin is a mixin class generator that accepts superclass as a parameter, and then returns a subclass that inherits superclass, and this subclass contains a foo method.

Then, the target class inherits this mixin class, and achieves the purpose of "mixing" the foo method.

class MyClass extends MyMixin(MyBaseClass) {
  /* ... */
}

let c = new MyClass();
c.foo(); // "foo from MyMixin"

If you need to "mix in" multiple methods, generate multiple mixin classes.

class MyClass extends Mixin1(Mixin2(MyBaseClass)) {
  /* ... */
}

One advantage of this way of writing is that you can call super, so you can avoid overwriting methods of the same name of the parent class during the "mixing" process.

let Mixin1 = (superclass) =>
  class extends superclass {
    foo() {
      console.log("foo from Mixin1");
      if (super.foo) super.foo();
    }
  };

let Mixin2 = (superclass) =>
  class extends superclass {
    foo() {
      console.log("foo from Mixin2");
      if (super.foo) super.foo();
    }
  };

class S {
  foo() {
    console.log("foo from S");
  }
}

class C extends Mixin1(Mixin2(S)) {
  foo() {
    console.log("foo from C");
    super.foo();
  }
}

In the above code, every time a mix-in occurs, the super.foo method of the parent class is called. As a result, the same-named method of the parent class is not overwritten and the behavior is retained.

new C().foo();
// foo from C
// foo from Mixin1
// foo from Mixin2
// foo from S

Trait

Trait is also a decorator, the effect is similar to that of Mixin, but it provides more functions, such as preventing conflicts of methods with the same name, excluding certain methods from mixing, and giving aliases to mixed methods, and so on.

The following uses traits-decorator this third-party module as an example. The traits decorator provided by this module can not only accept objects, but also ES6 classes as parameters.

import { traits } from "traits-decorator";

class TFoo {
  foo() {
    console.log("foo");
  }
}

const TBar = {
  bar() {
    console.log("bar");
  },
};

@traits(TFoo, TBar)
class MyClass {}

let obj = new MyClass();
obj.foo(); // foo
obj.bar(); // bar

In the above code, through the traits decorator, the foo method of the TFoo class and the bar method of the TBar object are "mixed" on the MyClass class.

Trait does not allow "mixing in" methods with the same name.

import { traits } from "traits-decorator";

class TFoo {
  foo() {
    console.log("foo");
  }
}

const TBar = {
  bar() {
    console.log("bar");
  },
  foo() {
    console.log("foo");
  },
};

@traits(TFoo, TBar)
class MyClass {}
// report an error
// throw new Error('Method named: '+ methodName +' is defined twice.');
// ^
// Error: Method named: foo is defined twice.

In the above code, both TFoo and TBar have foo methods, and as a result, the traits decorator reports an error.

One solution is to exclude the foo method of TBar.

import { traits, excludes } from "traits-decorator";

class TFoo {
  foo() {
    console.log("foo");
  }
}

const TBar = {
  bar() {
    console.log("bar");
  },
  foo() {
    console.log("foo");
  },
};

@traits(TFoo, TBar::excludes("foo"))
class MyClass {}

let obj = new MyClass();
obj.foo(); // foo
obj.bar(); // bar

The above code uses the binding operator (::) to exclude the foo method on the TBar, and no error will be reported when mixed in.

Another way is to give an alias to the foo method of TBar.

import { traits, alias } from "traits-decorator";

class TFoo {
  foo() {
    console.log("foo");
  }
}

const TBar = {
  bar() {
    console.log("bar");
  },
  foo() {
    console.log("foo");
  },
};

@traits(TFoo, TBar::alias({ foo: "aliasFoo" }))
class MyClass {}

let obj = new MyClass();
obj.foo(); // foo
obj.aliasFoo(); // foo
obj.bar(); // bar

The above code is the alias of aliasFoo for the foo method of TBar, so MyClass can also be mixed into the foo method of TBar.

The alias and excludes methods can be used in combination.

@traits(TExample::excludes("foo", "bar")::alias({ baz: "exampleBaz" }))
class MyClass {}

The above code excludes the foo method and the bar method of TExample, and creates the alias exampleBaz for the baz method.

The as method provides another way to write the above code.

@traits(
  TExample::as({ excludes: ["foo", "bar"], alias: { baz: "exampleBaz" } })
)
class MyClass {}