May 08, 2021 ES6
4. 3. Why can't decorators be used for functions?
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.
@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 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.
Decorators can be used to decorate the entire class.
@testable
class MyTestableClass {
// ...
}
function testable(target) {
target.isTestable = true;
}
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.
@decorator
class A {}
// 等同于
class A {}
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.
function testable(target) {
// ...
}
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.
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 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.
function testable(target) {
target.prototype.isTestable = true;
}
@testable
class MyTestableClass {}
let obj = new MyTestableClass();
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.
// 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. This feature can be simulated with Object.assign().
const Foo = {
foo() { console.log('foo') }
};
class MyClass {}
Object.assign(MyClass.prototype, Foo);
let obj = new MyClass();
obj.foo() // 'foo'
In real-world development, react and Redux libraries often need to be written like this.
class MyReactComponent extends React.Component {}
export default connect(mapStateToProps, mapDispatchToProps)(MyReactComponent);
With the decorator, you can rewrite the code above.
@connect(mapStateToProps, mapDispatchToProps)
export default class MyReactComponent extends React.Component {}
The 3d is relatively easy to understand.
Decorators can decorate not only classes, but also properties of classes.
class Person {
@readonly
name() { return `${this.first} ${this.last}` }
}
In the code above, the decorator
readonly
used to decorate the name method of
name
The decorator
readonly
accepts a total
三个
function readonly(target, name, descriptor){
// descriptor对象原来的值如下
// {
// value: specifiedFunction,
// enumerable: false,
// configurable: true,
// writable: true
// };
descriptor.writable = false;
return descriptor;
}
readonly(Person.prototype, 'name', descriptor);
// 类似于
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.
class Person {
@nonenumerable
get kidCount() { return this.children.length; }
}
function nonenumerable(target, name, descriptor) {
descriptor.enumerable = false;
return descriptor;
}
The
@log
that acts as an output log.
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 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.
@testable
class Person {
@readonly
@nonenumerable
name() { return ${this.first} ${this.last} }
}
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.
@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 onions, and then execute from the inside out.
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 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.
Decorators can only be used for classes and methods of classes, not for functions, because there is function elevation.
var counter = 0;
var add = function () {
counter++;
};
@add
function foo() {
}
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.
@add
function foo() {
}
var counter;
var add;
counter = 0;
add = function () {
counter++;
};
Here's another example.
var readOnly = require("some-decorator");
@readOnly
function foo() {
}
The above code also has problems, because the actual execution is like this.
var readOnly;
@readOnly
function foo() {
}
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.
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 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.
import { autobind } from 'core-decorators';
class Person {
@autobind
getPerson() {
return this;
}
}
let person = new Person();
let getPerson = person.getPerson;
getPerson() === person;
// true
(2)@readonly
readonly
make properties or methods uncrote.
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
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.
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)
deprecate
deprecated
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
suppressWarnings
decorator suppresses console.warn() calls caused by the
deprecated
decorator.
However, calls made by asynchronous code are excluded.
import { suppressWarnings } from 'core-decorators';
class Person {
@deprecated
facepalm() {}
@suppressWarnings
facepalmWithoutWarning() {
this.facepalm();
}
}
let person = new Person();
person.facepalmWithoutWarning();
// no warning is logged
We can use the decorator to automatically emit an event when the method of the object 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('频道: ', channelName);
console.log('事件: ', topic);
console.log('数据: ', v);
});
return function(target, name, descriptor) {
const fn = descriptor.value;
descriptor.value = function() {
let value = fn.apply(this, arguments);
msgChannel.publish(topic, value);
};
};
}
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.
// 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();
Later, whenever you call someMethod or anotherMethod, an event is automatically issued.
$ bash-node index.js
频道: component
事件: foo.some.message
数据: { my: 'data' }
频道: /
事件: foo.some.other
数据: undefined
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.
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 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.
export function mixins(...list) {
return function (target) {
Object.assign(target.prototype, ...list);
};
}
You can then use the decorator above 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"
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.
class MyClass extends MyBaseClass {
/* ... */
}
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.
let MyMixin = (superclass) => class extends superclass {
foo() {
console.log('foo from MyMixin');
}
};
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.
class MyClass extends MyMixin(MyBaseClass) {
/* ... */
}
let c = new MyClass();
c.foo(); // "foo from MyMixin"
If you need to "mix in" multiple methods, you generate multiple blending classes.
class MyClass extends Mixin1(Mixin2(MyBaseClass)) {
/* ... */
}
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.
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, 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.
new C().foo()
// foo from C
// foo from Mixin1
// foo from Mixin2
// foo from S
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.
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 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.
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 { }
// 报错
// throw new Error('Method named: ' + methodName + ' is defined twice.');
// ^
// 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.
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 (::) 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.
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 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.
@traits(TExample::excludes('foo','bar')::alias({baz:'exampleBaz'}))
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.
@traits(TExample::as({excludes:['foo', 'bar'], alias: {baz: 'exampleBaz'}}))
class MyClass {}