装饰器(Decorator)
Decorator 提案经过了大幅修改,目前还没有定案,不知道语法会不会再变。下面的内容完全依据以前的提案,已经有点过时了。等待定案以后,需要完全重写。
装饰器(Decorator)是一种与类(class)相关的语法,用来注释或修改类和类方法。许多面向对象的语言都有这项功能,目前有一个提案将其引入了 ECMAScript。
装饰器是一种函数,写成@ + 函数名。它可以放在类和类方法、属性的定义前面。
@frozen class Foo {
@configurable(false)
@enumerable(true)
method() {}
@throttle(500)
expensiveMethod() {}
}更多知识可参考:http://es6.ruanyifeng.com/#docs/decorator
应用场景
其实很多时候装饰器可以充当高阶函数的作用,用来给原来的函数或者类增加一些新的能力。最近开发中遇到了一个应用场景,就采用装饰器来实践下。
ant design里,input、select 等输入组件,都有onchange方法,但是比较麻烦是是,input 的onchange第一个参数是value,即输入的值;而select 的onchange第一个参数是event,需要多写一行代码,value=event.target.value,最重要的是,很多组件,我根本记不住哪个组件是value,哪个是event,多数时候需要打印出来看一下。实在不能忍,突然想到了装饰器,是不是我们可以写一个装饰器,统一把第一个参数归一化为value呢?当然可以。
如何写一个装饰器呢
装饰器写法很简单,它接受三个参数,并且返回descriptor:
- target:当前类的原型
- name:当前方法/属性的名字
- descriptor:将被定义或修改的属性描述符,可参考Object.defineProperty
descriptor.value 就是被修饰的方法,官方大致的思路很明确,就是利用常规的 hook 方法,对 value 进行 hook 改造,达到装饰的目的。
我们要写的装饰器大概形状应该是这样的。
function formatValue (target, name, descriptor) {
// ...something
return {
...descriptor,
value: function() {
// ...
}
}
}开始写代码
参考了网上一些文章,开始写其实很简单,马上就能实现我们想要的代码。
function formatValue (target, name, descriptor) {
const fn = descriptor.value;
return {
...descriptor,
value:(...args) =>{
const len = args.length;
if (len > 0 && args[len - 1].target) {
args[len - 1] = args[len - 1].target.value;
}
fn.apply(this, args);
}
}
}组件代码
class extends React.PureComponent {
@formatValue
handleActionChange=(value)=> {
this.setState({action: value});
}
render () {}
}然而这段代码并不work, 把descriptor 打印出来看,发现并没有value 属性,倒是多了个initializer,什么鬼?
{
configurable: true,
enumerable: true,
writable: true,
initializer:f()
}查资料并且结合着猜想,会不会是因为不支持箭头函数?果然,当我把handleActionChange改为普通函数声明的时候,value 值就能拿到了,但initializer不见了?事情有些蹊跷,接着猜想,会不会是因为箭头函数的定义方式,其实是一个赋值表达式,并不是函数声明表达式,本质是把一个匿名函数赋值给了handleActionChange这个属性。那会不会是装饰器对属性的装饰和对方法的装饰有区别呢?顺着这个思路查资料,果然找到了答案。
装饰器在装饰属性的时候,先通过执行调用 initializer 来获取属性默认值(此时是不存在 value 的),然后配合 getter 和 setter 来装饰这个属性本身。所以我们修改代码如下:
export function formatInputValue (target, name, descriptor) {
const fn = descriptor.initializer;
return {
...descriptor,
initializer: (...args) =>{
const len = args.length;
if (len > 0 && args[len - 1].target) {
args[len - 1] = args[len - 1].target.value;
}
fn.apply(this, args);
}
};
}但其实这个函数还有问题,因为箭头函数绑定的this,是定义时的this,普通函数里为undefined。而我们其实要的效果,是绑定了组件执行上下文的this,所以修改为:
export function formatInputValue (target, name, descriptor) {
const fn = descriptor.initializer;
return {
...descriptor,
initializer: function (...args) {
const len = args.length;
if (len > 0 && args[len - 1].target) {
args[len - 1] = args[len - 1].target.value;
}
fn.apply(this, args);
}
};
}现在终于可以执行成功了。但这个函数还是比较局限的,因为目前只有被装饰的函数是箭头函数才能正确返回,如果装饰的的普通的函数方法,那还是不符合预期,所以我们再兼容一下。
export function formatInputValue (target, name, descriptor) {
let fn;
function newFn (...args) {
const len = args.length;
if (len > 0 && args[len - 1].target) {
args[len - 1] = args[len - 1].target.value;
}
fn.apply(this, args);
}
// 装饰属性的情况下,以此可以获取实例化的时候此属性的默认值
if (descriptor.initializer) {
return {
...descriptor,
initializer: function () {
// 不用箭头函数,绑定运行时的this
fn = descriptor.initializer.call(this);
return newFn;
},
};
} else {
// 装饰方法的情况下
fn = descriptor.value;
return {
...descriptor,
value: newFn,
};
}
}这样就可以愉快的使用装饰器了。最后要注意的一点就是this 的指向问题,只有initializer和value对应的函数执行上下文是是装饰器调用执行上下文。
参考链接