ES6 Decorator, core decorators js,Mixin,Trait

Posted by stemp on Sun, 26 Dec 2021 11:50:09 +0100

ES6 (xxx) Decorator, core-decorators.js, Mixin, Trait

[note] the Decorator proposal has been greatly revised and has not been finalized yet. I wonder if the grammar will change again. The following content is completely based on previous proposals and is a little outdated. After waiting for finalization, it needs to be completely rewritten.

Summary:

  • ==Decorator is a class related syntax used to annotate or modify classes and class methods proposal It is introduced into ECMAScript.
  • Decorator is a function written as @ + function name. It can be placed in front of the definitions of classes and class methods.
  • Decorators are essentially functions that execute at compile time.
  • Decorators can only be used for classes and methods of classes, not functions, because function promotion exists.
@frozen class Foo {
  @configurable(false)
  @enumerable(true)
  method() {}

  @throttle(500)
  expensiveMethod() {}
}

The above code uses a total of four decorators, one for the class itself and the other three for class methods. They not only increase the readability of the code and clearly express the intention, but also provide a convenient means to add or modify the function of the class.

1. Decoration of class

Decorators can be used to decorate an entire class.

@testable
class MyTestableClass {
  // ...
}

//Decorator function, decorator is a function that processes a class. The first parameter of the decorator function is the target class to be decorated.
function testable(target) {
  target.isTestable = true;
}

MyTestableClass.isTestable // true

In the above code, @ testable is a decorator. It modifies the behavior of the MyTestableClass class by adding the static attribute isTestable. The parameter target of the TestTable function is the MyTestableClass itself.

Basically, the behavior of the decorator is as follows.

@decorator
class A {}

// Equivalent to

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

That is, a decorator is a function that processes a 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 to be decorated.

If you think a parameter is not enough, you can encapsulate another layer of functions 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 means that the behavior of the decorator can be modified.

Note that the decorator changes the behavior of the class when the code is compiled, not at run time. This means that the decorator can run code during the compilation phase. That is, decorators are essentially functions that execute at compile time.

The previous example is to add a static attribute to the class. If you want to add an instance attribute, you can operate 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 Foo object to the instance of MyClass through the decorator mixins. You can use object Assign () simulates this function.

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 library, it often needs to be written as follows.

class MyReactComponent extends React.Component {}

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

With the decorator, you can rewrite the above code.

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

Relatively speaking, the latter seems easier to understand.

2. Decoration method

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

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 a total of three parameters.

//The readonly will modify the descriptor of the attribute, and then the modified descriptor will be used to define the attribute.

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);
// be 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 (this is the prototype). The decorator is intended to "decorate" the instance of the class, but the instance has not been generated at this time, so it can only decorate the prototype (this is different from the decoration of the class. In that case, the target parameter refers to the class itself); the second parameter is the attribute name to be decorated, and the third parameter is the description object of the attribute.

In addition, as described in the above code, the readonly will modify the descriptor of the attribute, and then the modified descriptor will be used to define the attribute.

The following is another example. Modify the enumerable property of the property description object so that the property is not traversable.

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

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

The following @ log decorator can output 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 @ log decorator is used to execute the console once before executing the original operation Log to achieve the purpose of outputting logs.

The decorator has the function of annotation.

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

The following is written using Decorator assembly , it 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 in the same method, it will enter from the outside to the inside like peeling an onion, and then execute from the inside to the outside.

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

//First enter from outside to inside, and then execute from inside to outside
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.

Besides annotations, decorators can also be used for type checking. Therefore, this function 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 functions, because they exist Function promotion.

When a function is declared with the function command (i.e. function f() {}), the entire function will be promoted to the code header like a variable declaration. The function f declared later is overwritten by the previously declared function because the function promotion was declared first.

var counter = 0;

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

@add
function foo() {
}

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

//Variable promotion
var counter;
var add;

//Function promotion
@add
function foo() {
}

counter = 0;

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

Here is another example.

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

@readOnly
function foo() {
}

There is also a problem with the above code, because the actual implementation is as follows.

var readOnly;

@readOnly
function foo() {
}

readOnly = require("some-decorator");

In short, the decorator cannot be used for functions due to function promotion. 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 high-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);

4. core-decorators.js

core-decorators.js Is a third-party module, which provides several common decorators, through which you can better understand the decorator.

(1)@autobind

<u> The autobind decorator causes the this object in the method to bind to 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 a property or method non writable.

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 overrides the method of the same name of the parent class. If not, 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 that the method will be abolished.

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 the console caused by the deprecated decorator Warn() call. 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

5. Use decorator to automatically publish 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 the descriptor Value, so that an event will be automatically issued when the original method is called. 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, as long as someMethod or othermethod is called, an event will be automatically issued.

$ bash-node index.js
 channel:  component
 event:  foo.some.message
 data:  { my: 'data' }

channel:  /
event:  foo.some.other
 data:  undefined

6. Mixin

Based on the decorator, Mixin mode can be realized. The so-called Mixin mode is an alternative to object inheritance, which is translated as "mix in" in Chinese, which means the method of mixing one object with another.

Look at 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 object The assign method can "mix" the foo method into the MyClass class, resulting in the instance obj objects of MyClass having the foo method. This is a simple implementation of the "blend in" pattern.

Next, we deploy a generic script mixins JS, write Mixin as a decorator.

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

Then, you can use the decorator above to "mix" 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 mixins, this decorator implements the foo method of "mixing" foo objects on MyClass class.

However, the above method will overwrite the prototype object of 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" a foo method into MyClass, one way is to insert a mixed class between MyClass and MyBaseClass. This class has foo methods and inherits all methods of MyBaseClass, 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 mixed class generator, which takes superclass as a parameter, and then returns a subclass inheriting superclass, which contains a foo method.

Then, the target class inherits the mixed class to achieve the purpose of "mixing" the foo method.

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

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

If multiple methods need to be "mixed", multiple mixed classes are generated.

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

One advantage of this writing method is that super can be called, so it can avoid overwriting the method with the same name of the parent class in 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, super of the parent class is called every time a blend occurs Foo method, so that the method with the same name of the parent class is not overwritten, and the behavior is preserved.

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

7. Trait

Trail is also a decorator. Its effect is similar to Mixin, but it provides more functions, such as preventing the conflict of methods with the same name, excluding some mixed methods, aliasing mixed methods, and so on.

Use below traits-decorator This third-party module is used as an example. The traits decorator provided by this module can accept not only 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, the foo method of TFoo class and the bar method of TBar object are "mixed" into MyClass through the traits decorator.

Trail does not allow "mixing" 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 errors
// 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 the trails 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 TBar, so there will be no error when mixing in.

Another way is to alias 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 aliases the foo method of TBar, so MyClass can also be mixed into the foo method of TBar.

alias and excludes methods can be used together.

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

The above code excludes the foo method and bar method of TExample and aliases the baz method as exampleBaz.

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

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

Topics: Javascript Front-end ECMAScript