title: ES6 - 装饰器decorator
date: 2020.12.13
top:
tags:
类的装饰
方法的装饰
为什么装饰器不能用于函数?
core-decorators.js
使用装饰器实现自动发布事件
Mixin
Trait
2020.12.13 星期日 10:40
[说明] Decorator 提案经过了大幅修改,目前还没有定案,不知道语法会不会再变。
装饰器(Decorator)是一种与类(class)相关的语法,用来注释或修改类和类方法。
装饰器是一种函数,写成@ + 函数名。它可以放在类和类方法的定义前面。
@frozen class Foo {
@configurable(false)
@enumerable(true)
method() {}
@throttle(500)
expensiveMethod() {}
}
$_PS: 装饰者模式。babel插件:@babel/plugin-proposal-decorators
注意,装饰器对类的行为的改变,是代码编译时发生的,而不是在运行时装饰器本质就是编译时执行的函数。
@testable
class MyTestableClass {
// ...
}
// ## 1.1 装饰器函数的第一个参数,就是所要装饰的目标类。
function testable(target) {
// ...
target.isTestable = true;
}
// ## 1.2 如果觉得一个参数不够用,可以在装饰器外面再封装一层函数。
// 装饰器testable可以接受参数,这就等于可以修改装饰器的行为。
function testable(isTestable) {
return function(target) {
target.isTestable = isTestable;
}
}
@testable(true) // @testable(false)
class MyTestableClass {}
MyTestableClass.isTestable // true
// 前面的例子是为类添加一个静态属性,
// ## 2 如果想添加实例属性,可以通过目标类的prototype对象操作。
function testable(target) {
target.prototype.isTestable = true;
}
// ### 2.1
// 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'
// 上面代码通过装饰器mixins,把Foo对象的方法添加到了MyClass的实例上面。
// ### 2.2 可以用Object.assign()模拟这个功能。
// ### 2.3 实际开发中,React 与 Redux 库结合使用时,常常需要写成下面这样。
class MyReactComponent extends React.Component {}
export default connect(mapStateToProps, mapDispatchToProps)(MyReactComponent);
// 有了装饰器,就可以改写上面的代码。
@connect(mapStateToProps, mapDispatchToProps)
export default class MyReactComponent extends React.Component {}
装饰器不仅可以装饰类,还可以装饰类的属性。
// ## 1 装饰器函数readonly一共可以接受三个参数。
// 装饰器第一个参数是类的原型对象,
class Person {
@readonly
name() { return `${this.first} ${this.last}` }
}
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);
// ## 1.2 修改属性描述对象的enumerable属性
// ## 1.3 下面的@log装饰器,可以起到输出日志的作用。
// ## 2 装饰器有注释的作用。
@testable
class Person {
@readonly
@nonenumerable
name() { return `${this.first} ${this.last}` }
}
// ### 下面是使用 Decorator 写法的组件,看上去一目了然。
@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>
);
}
}
// 如果同一个方法有多个装饰器,会像剥洋葱一样,先从外到内进入,然后由内向外执行。
// ## 3 除了注释,装饰器还能用来类型检查。
// 所以,对于类来说,这项功能相当有用。从长期来看,它将是 JavaScript 代码静态分析的重要工具。
装饰器只能用于类和类的方法,不能用于函数,因为存在函数提升。
另一方面,如果一定要装饰函数,可以采用高阶函数的形式直接执行。
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是一个第三方模块,提供了几个常见的装饰器,通过它可以更好地理解装饰器。
(1)@autobind
(2)@readonly
(3)@override
(4)@deprecate (别名@deprecated)
(5)@suppressWarnings
它使用的事件“发布/订阅”库是Postal.js。
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);
};
};
}
在装饰器的基础上,可以实现Mixin模式。
export function mixins(...list) {
return function (target) {
Object.assign(target.prototype, ...list);
};
}
不过,上面的方法会改写MyClass类的prototype对象,如果不喜欢这一点,也可以通过类的继承实现 Mixin。
let MyMixin = (superclass) => class extends superclass {
foo() {
console.log('foo from MyMixin');
}
};
class MyClass extends MyMixin(MyBaseClass) {
/* ... */
}
let c = new MyClass();
c.foo(); // "foo from MyMixin"
// ### 如果需要“混入”多个方法,就生成多个混入类。
class MyClass extends Mixin1(Mixin2(MyBaseClass)) {
/* ... */
}
这种写法的一个好处,是可以调用super,因此可以避免在“混入”过程中覆盖父类的同名方法。
Trait 也是一种装饰器,效果与 Mixin 类似,但是提供更多功能,比如防止同名方法的冲突、排除混入某些方法、为混入的方法起别名等等。
下面采用traits-decorator这个第三方模块作为例子。这个模块提供的traits装饰器,不仅可以接受对象,还可以接受 ES6 类作为参数。
上面代码中,TFoo和TBar都有foo方法,结果traits装饰器报错。
一种解决方法是排除TBar的foo方法。
另一种方法是为TBar的foo方法起一个别名。
alias和excludes方法,可以结合起来使用。
as方法则为上面的代码提供了另一种写法。
11.25