Coding With Fun
Home Docker Django Node.js Articles Python pip guide FAQ Policy

Decorator


May 08, 2021 ES6


Table of contents


Brief introduction

The Decorator proposal has been significantly revised and has not yet been finalized, and I do not know if the grammar will change again. T he following is somewhat outdated, based entirely on previous proposals. After waiting for the final decision, it needs to be completely rewritten.

Decorator is a class-related syntax used to comment or modify classes and methods. This feature is available in many object-oriented languages, and there is currently a proposal to introduce it into ECMAScript.

An decorator is a function that is written as @ + 函数名 It can be placed before the definition of classes and class methods.

  1. @frozen class Foo {
  2. @configurable(false)
  3. @enumerable(true)
  4. method() {}
  5. @throttle(500)
  6. expensiveMethod() {}
  7. }

The above code uses a total of four decorators, one for the class itself and three for the class method. Not only do they increase the readability of the code, they clearly express intent, but they also provide a convenient means to add or modify the functionality of the class.

1. Class decoration

Decorators can be used to decorate the entire class.

  1. @testable
  2. class MyTestableClass {
  3. // ...
  4. }
  5. function testable(target) {
  6. target.isTestable = true;
  7. }
  8. MyTestableClass.isTestable // true

In the code @testable is a decorator. I t modifies the MyTestableClass class, adding a static property isTestable testable of the target is the MyTestableClass itself.

Basically, the behavior of the decorator is like this.

  1. @decorator
  2. class A {}
  3. // 等同于
  4. class A {}
  5. A = decorator(A) || A;

That is, the decorator is a function that processes classes. The first argument of the decorator function is the target class to be decorated.

  1. function testable(target) {
  2. // ...
  3. }

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

If you feel that one parameter is not sufficient, you can encapsulate another layer of functions outside the decorator.

  1. function testable(isTestable) {
  2. return function(target) {
  3. target.isTestable = isTestable;
  4. }
  5. }
  6. @testable(true)
  7. class MyTestableClass {}
  8. MyTestableClass.isTestable // true
  9. @testable(false)
  10. class MyClass {}
  11. MyClass.isTestable // false

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

Note that the decorator's changes to the behavior of the class occur when the code is compiled, not at runtime. T his means that the decorator can run the code during the compilation phase. That is, the decorator is essentially a function that is executed at compile time.

The previous example is to add a static property to the class, and if you want to add an instance property, you can do so through the prototype object of the target class.

  1. function testable(target) {
  2. target.prototype.isTestable = true;
  3. }
  4. @testable
  5. class MyTestableClass {}
  6. let obj = new MyTestableClass();
  7. obj.isTestable // true

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

Here's another example.

  1. // mixins.js
  2. export function mixins(...list) {
  3. return function (target) {
  4. Object.assign(target.prototype, ...list)
  5. }
  6. }
  7. // main.js
  8. import { mixins } from './mixins'
  9. const Foo = {
  10. foo() { console.log('foo') }
  11. };
  12. @mixins(Foo)
  13. class MyClass {}
  14. let obj = new MyClass();
  15. obj.foo() // 'foo'

The above code adds the method of the Foo object to the instance of MyClass through the decorator mixins. This feature can be simulated with Object.assign().

  1. const Foo = {
  2. foo() { console.log('foo') }
  3. };
  4. class MyClass {}
  5. Object.assign(MyClass.prototype, Foo);
  6. let obj = new MyClass();
  7. obj.foo() // 'foo'

In real-world development, react and Redux libraries often need to be written like this.

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

With the decorator, you can rewrite the code above.

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

The 3d is relatively easy to understand.

2. Method of decoration

Decorators can decorate not only classes, but also properties of classes.

  1. class Person {
  2. @readonly
  3. name() { return `${this.first} ${this.last}` }
  4. }

In the code above, the decorator readonly used to decorate the name method of name

The decorator readonly accepts a total 三个

  1. function readonly(target, name, descriptor){
  2. // descriptor对象原来的值如下
  3. // {
  4. // value: specifiedFunction,
  5. // enumerable: false,
  6. // configurable: true,
  7. // writable: true
  8. // };
  9. descriptor.writable = false;
  10. return descriptor;
  11. }
  12. readonly(Person.prototype, 'name', descriptor);
  13. // 类似于
  14. Object.defineProperty(Person.prototype, 'name', descriptor);

The first parameter of the decorator is the prototype object of the class, the Person.prototype the decorator is intended to "decorate" the class instance, but this time the instance has not yet been generated, so can only go to decorate the target refers to the class itself);

In addition, the code above states that the decorator modifies the description object (descriptor) of the property, which is then used to define the property.

Here is another example of modifying the enumerable a property so that it cannot be traversed.

  1. class Person {
  2. @nonenumerable
  3. get kidCount() { return this.children.length; }
  4. }
  5. function nonenumerable(target, name, descriptor) {
  6. descriptor.enumerable = false;
  7. return descriptor;
  8. }

The @log that acts as an output log.

  1. class Math {
  2. @log
  3. add(a, b) {
  4. return a + b;
  5. }
  6. }
  7. function log(target, name, descriptor) {
  8. var oldValue = descriptor.value;
  9. descriptor.value = function() {
  10. console.log( Calling ${name} with , arguments);
  11. return oldValue.apply(this, arguments);
  12. };
  13. return descriptor;
  14. }
  15. const math = new Math();
  16. // passed parameters should get logged now
  17. math.add(2, 4);

In the code above, @log adornor is to perform a console test before performing the console.log the purpose of outputing the log.

The decorator has the effect of annotation.

  1. @testable
  2. class Person {
  3. @readonly
  4. @nonenumerable
  5. name() { return ${this.first} ${this.last} }
  6. }

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

Here are the components that use Decorator's writing to look at a glance.

  1. @Component({
  2. tag: 'my-component',
  3. styleUrl: 'my-component.scss'
  4. })
  5. export class MyComponent {
  6. @Prop() first: string;
  7. @Prop() last: string;
  8. @State() isVisible: boolean = true;
  9. render() {
  10. return (
  11. <p>Hello, my name is {this.first} {this.last}</p>
  12. );
  13. }
  14. }

If there are multiple decorators in the same method, it will enter from the outside to the inside, like peeling onions, and then execute from the inside out.

  1. function dec(id){
  2. console.log('evaluated', id);
  3. return (target, property, descriptor) => console.log('executed', id);
  4. }
  5. class Example {
  6. @dec(1)
  7. @dec(2)
  8. method(){}
  9. }
  10. // evaluated 1
  11. // evaluated 2
  12. // executed 2
  13. // executed 1

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

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

3. Why can't decorators be used for functions?

Decorators can only be used for classes and methods of classes, not for functions, because there is function elevation.

  1. var counter = 0;
  2. var add = function () {
  3. counter++;
  4. };
  5. @add
  6. function foo() {
  7. }

The above code is intended to be that after execution counter is equal to 1, but the actual result is that counter is equal to 0. Because the function is promoted, the code that is actually executed is like this.

  1. @add
  2. function foo() {
  3. }
  4. var counter;
  5. var add;
  6. counter = 0;
  7. add = function () {
  8. counter++;
  9. };

Here's another example.

  1. var readOnly = require("some-decorator");
  2. @readOnly
  3. function foo() {
  4. }

The above code also has problems, because the actual execution is like this.

  1. var readOnly;
  2. @readOnly
  3. function foo() {
  4. }
  5. readOnly = require("some-decorator");

In short, because of the function elevation, the decorator cannot be used for functions. Classes don't ascend, so there's no problem with that.

On the other hand, if you must decorate the function, you can use the form of higher-order functions to perform directly.

  1. function doSomething(name) {
  2. console.log('Hello, ' + name);
  3. }
  4. function loggingDecorator(wrapped) {
  5. return function() {
  6. console.log('Starting');
  7. const result = wrapped.apply(this, arguments);
  8. console.log('Finished');
  9. return result;
  10. }
  11. }
  12. const wrapped = loggingDecorator(doSomething);

4. core-decorators.js

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

(1)@autobind

autobind decorator makes the this object this method binding the original object.

  1. import { autobind } from 'core-decorators';
  2. class Person {
  3. @autobind
  4. getPerson() {
  5. return this;
  6. }
  7. }
  8. let person = new Person();
  9. let getPerson = person.getPerson;
  10. getPerson() === person;
  11. // true

(2)@readonly

readonly make properties or methods uncrote.

  1. import { readonly } from 'core-decorators';
  2. class Meal {
  3. @readonly
  4. entree = 'steak';
  5. }
  6. var dinner = new Meal();
  7. dinner.entree = 'salmon';
  8. // Cannot assign to read only property 'entree' of [object Object]

(3)@override

override checks the method of the child class, correctly overrides the parent class's method of the same name, and if it is incorrect, it will report an error.

  1. import { override } from 'core-decorators';
  2. class Parent {
  3. speak(first, second) {}
  4. }
  5. class Child extends Parent {
  6. @override
  7. speak() {}
  8. // SyntaxError: Child#speak() does not properly override Parent#speak(first, second)
  9. }
  10. // or
  11. class Child extends Parent {
  12. @override
  13. speaks() {}
  14. // SyntaxError: No descriptor matching Child#speaks() was found on the prototype chain.
  15. //
  16. // Did you mean "speak"?
  17. }

(4) @deprecate (alias @deprecated)

deprecate deprecated displays a warning on the console that the method will be abolished.

  1. import { deprecate } from 'core-decorators';
  2. class Person {
  3. @deprecate
  4. facepalm() {}
  5. @deprecate('We stopped facepalming')
  6. facepalmHard() {}
  7. @deprecate('We stopped facepalming', { url: 'http://knowyourmeme.com/memes/facepalm' })
  8. facepalmHarder() {}
  9. }
  10. let person = new Person();
  11. person.facepalm();
  12. // DEPRECATION Person#facepalm: This function will be removed in future versions.
  13. person.facepalmHard();
  14. // DEPRECATION Person#facepalmHard: We stopped facepalming
  15. person.facepalmHarder();
  16. // DEPRECATION Person#facepalmHarder: We stopped facepalming
  17. //
  18. // See http://knowyourmeme.com/memes/facepalm for more details.
  19. //

(5)@suppressWarnings

suppressWarnings decorator suppresses console.warn() calls caused by the deprecated decorator. However, calls made by asynchronous code are excluded.

  1. import { suppressWarnings } from 'core-decorators';
  2. class Person {
  3. @deprecated
  4. facepalm() {}
  5. @suppressWarnings
  6. facepalmWithoutWarning() {
  7. this.facepalm();
  8. }
  9. }
  10. let person = new Person();
  11. person.facepalmWithoutWarning();
  12. // no warning is logged

5. Use the decorator to automatically publish events

We can use the decorator to automatically emit an event when the method of the object is called.

  1. const postal = require("postal/lib/postal.lodash");
  2. export default function publish(topic, channel) {
  3. const channelName = channel || '/';
  4. const msgChannel = postal.channel(channelName);
  5. msgChannel.subscribe(topic, v => {
  6. console.log('频道: ', channelName);
  7. console.log('事件: ', topic);
  8. console.log('数据: ', v);
  9. });
  10. return function(target, name, descriptor) {
  11. const fn = descriptor.value;
  12. descriptor.value = function() {
  13. let value = fn.apply(this, arguments);
  14. msgChannel.publish(topic, value);
  15. };
  16. };
  17. }

The code above defines a publish which automatically emits an event when the original method is called by rewriting 'descriptor.value'. The event "publish/subscribe" library it uses is a Postal .js.

Its usage is as follows.

  1. // index.js
  2. import publish from './publish';
  3. class FooComponent {
  4. @publish('foo.some.message', 'component')
  5. someMethod() {
  6. return { my: 'data' };
  7. }
  8. @publish('foo.some.other')
  9. anotherMethod() {
  10. // ...
  11. }
  12. }
  13. let foo = new FooComponent();
  14. foo.someMethod();
  15. foo.anotherMethod();

Later, whenever you call someMethod or anotherMethod, an event is automatically issued.

  1. $ bash-node index.js
  2. 频道: component
  3. 事件: foo.some.message
  4. 数据: { my: 'data' }
  5. 频道: /
  6. 事件: foo.some.other
  7. 数据: undefined

6. Mixin

Mixin mode can be implemented on the Mixin decorator. The Mixin pattern is an alternative to object inheritance, and Chinese translates as mix in, meaning a way to blend into one object into another.

Take a look at the example below.

  1. const Foo = {
  2. foo() { console.log('foo') }
  3. };
  4. class MyClass {}
  5. Object.assign(MyClass.prototype, Foo);
  6. let obj = new MyClass();
  7. obj.foo() // 'foo'

In the above code, the object Foo has a foo method that allows you to "mix" the foo method into the MyClass class through the Object.assign method, resulting in instance obj objects of MyClass having foo methods. This is a simple implementation of the "mixed in" pattern.

Now, let's deploy a generic .js mixins and write Mixin as a decorator.

  1. export function mixins(...list) {
  2. return function (target) {
  3. Object.assign(target.prototype, ...list);
  4. };
  5. }

You can then use the decorator above to "mix in" various methods for the class.

  1. import { mixins } from './mixins';
  2. const Foo = {
  3. foo() { console.log('foo') }
  4. };
  5. @mixins(Foo)
  6. class MyClass {}
  7. let obj = new MyClass();
  8. obj.foo() // "foo"

The mixins decorator enables the foo method of "mixing" Foo objects above the MyClass class.

However, the above method overrides the prototype object of the MyClass class, and if you don't like it, you can also implement Mixin through the inheritance of the class.

  1. class MyClass extends MyBaseClass {
  2. /* ... */
  3. }

In the code above, MyClass inherits MyBaseClass. If we want to "mix in" a foo method in MyClass, one way is to insert a blended class between MyClass and MyBaseClass, which has a foo method and inherits all the methods of MyBaseClass, and then MyClass inherits the class.

  1. let MyMixin = (superclass) => class extends superclass {
  2. foo() {
  3. console.log('foo from MyMixin');
  4. }
  5. };

In the above code, MyMixin is a mixed class generator that accepts superclass as an argument, and then returns a subclass that inherits superclass, which contains a foo method.

The target class then inherits the mixed class, which achieves the goal of "mixing in" the foo method.

  1. class MyClass extends MyMixin(MyBaseClass) {
  2. /* ... */
  3. }
  4. let c = new MyClass();
  5. c.foo(); // "foo from MyMixin"

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

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

One benefit of this writing is that you can call super, so you can avoid overwriting the parent's method of the same name during the Blending process.

  1. let Mixin1 = (superclass) => class extends superclass {
  2. foo() {
  3. console.log('foo from Mixin1');
  4. if (super.foo) super.foo();
  5. }
  6. };
  7. let Mixin2 = (superclass) => class extends superclass {
  8. foo() {
  9. console.log('foo from Mixin2');
  10. if (super.foo) super.foo();
  11. }
  12. };
  13. class S {
  14. foo() {
  15. console.log('foo from S');
  16. }
  17. }
  18. class C extends Mixin1(Mixin2(S)) {
  19. foo() {
  20. console.log('foo from C');
  21. super.foo();
  22. }
  23. }

In the above code, each time a blend occurs, the parent's super.foo method is called, causing the parent class's method of the same name not to be overwritten and the behavior to be preserved.

  1. new C().foo()
  2. // foo from C
  3. // foo from Mixin1
  4. // foo from Mixin2
  5. // foo from S

7. Trait

Trait also a decorator that works like Mixin but provides more features, such as preventing conflicts with methods of the same name, excluding blending into certain methods, aliasing methods for mixing, and so on.

The third-party module traits-decorator is used as an example below. This module provides a traits decorator that accepts not only objects, but also ES6 classes as parameters.

  1. import { traits } from 'traits-decorator';
  2. class TFoo {
  3. foo() { console.log('foo') }
  4. }
  5. const TBar = {
  6. bar() { console.log('bar') }
  7. };
  8. @traits(TFoo, TBar)
  9. class MyClass { }
  10. let obj = new MyClass();
  11. obj.foo() // foo
  12. obj.bar() // bar

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

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

  1. import { traits } from 'traits-decorator';
  2. class TFoo {
  3. foo() { console.log('foo') }
  4. }
  5. const TBar = {
  6. bar() { console.log('bar') },
  7. foo() { console.log('foo') }
  8. };
  9. @traits(TFoo, TBar)
  10. class MyClass { }
  11. // 报错
  12. // throw new Error('Method named: ' + methodName + ' is defined twice.');
  13. // ^
  14. // Error: Method named: foo is defined twice.

In the code above, both TFoo and TBar have foo methods, and the traits decorator is wrong.

One workaround is to exclude the foo method for TBar.

  1. import { traits, excludes } from 'traits-decorator';
  2. class TFoo {
  3. foo() { console.log('foo') }
  4. }
  5. const TBar = {
  6. bar() { console.log('bar') },
  7. foo() { console.log('foo') }
  8. };
  9. @traits(TFoo, TBar::excludes('foo'))
  10. class MyClass { }
  11. let obj = new MyClass();
  12. obj.foo() // foo
  13. obj.bar() // bar

The above code uses the binding operator (::) exclude the foo method from the TBar, and the mix-in is not misalmissed.

Another approach is to start an alias for TBar's foo method.

  1. import { traits, alias } from 'traits-decorator';
  2. class TFoo {
  3. foo() { console.log('foo') }
  4. }
  5. const TBar = {
  6. bar() { console.log('bar') },
  7. foo() { console.log('foo') }
  8. };
  9. @traits(TFoo, TBar::alias({foo: 'aliasFoo'}))
  10. class MyClass { }
  11. let obj = new MyClass();
  12. obj.foo() // foo
  13. obj.aliasFoo() // foo
  14. obj.bar() // bar

The code above is alias alias alias for TBar's foo method, so MyClass can also blend into TBar's foo method.

The alias and excludes methods can be used together.

  1. @traits(TExample::excludes('foo','bar')::alias({baz:'exampleBaz'}))
  2. class MyClass {}

The above code excludes TExample's foo and bar methods and aliasses exampleBaz for the baz method.

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

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