class App extends React.Component {
}
connect(mapStateToProps, mapDispatchToProps)(App)
有了装饰器之后,connect 的写法可以变得更加优雅。
@connect(mapStateToProps, mapDispatchToProps)
class App extends React.Component {
}
3.4 类属性装饰器
类属性装饰器可以用在类的属性、方法、get/set 函数中,一般会接收三个参数:
target:被修饰的类
name:类成员的名字
descriptor:属性描述符,对象会将这个参数传给 Object.defineProperty
使用类属性装饰器可以做到很多有意思的事情,比如最开始举的那个 readonly 的例子:
function readonly(target, name, descriptor) {
descriptor.writable = false;
return descriptor;
}
class Person {
@readonly name = 'person'
}
const person = new Person();
person.name = 'tom';
还可以用来统计一个函数的执行时间,以便于后期做一些性能优化。
function time(target, name, descriptor) {
const func = descriptor.value;
if (typeof func === 'function') {
descriptor.value = function(...args) {
console.time();
const results = func.apply(this, args);
console.timeEnd();
return results;
}
}
}
class Person {
@time
say() {
console.log('hello')
}
}
const person = new Person();
person.say();
const validate = (type) => (target, name) => {
if (typeof target[name] !== type) {
throw new Error(`attribute ${name} must be ${type} type`)
}
}
class Form {
@validate('string')
static name = 111 // Error: attribute name must be ${type} type
}
如果你觉得对属性一个个手动去校验太过麻烦,也可以通过编写校验规则,来对整个类进行校验。
const rules = {
name: 'string',
password: 'string',
age: 'number'
}
const validator = rules => targetClass => {
return new Proxy(targetClass, {
construct(target, args) {
const obj = new target(...args);
for (let [name, type] of Object.entries(rules)) {
if (typeof obj[name] !== type) {
throw new Error(`${name} must be ${type}`)
}
}
return obj;
}
})
}
@validator(rules)
class Person {
name = 'tom'
password = '123'
age = '21'
}
const person = new Person();
1. 前言
装饰器是最新的 ECMA 中的一个提案,是一种与类(
class
)相关的语法,用来注释或修改类和类方法。装饰器在 Python 和 Java 等语言中也被大量使用。装饰器是实现 AOP(面向切面)编程的一种重要方式。下面是一个使用装饰器的简单例子,这个
@readonly
可以将count
属性设置为只读。可以看出来,装饰器大大提高了代码的简洁性和可读性。由于浏览器还未支持装饰器,为了让大家能够正常看到效果,这里我使用 Parcel 进行了一下简单的配置,可以去 clone 这个仓库后再来运行本文涉及到的所有例子,仓库地址:learn es6
2. 装饰器模式
在开始讲解装饰器之前,先从经典的装饰器模式说起。装饰器模式是一种结构型设计模式,它允许向一个现有的对象添加新的功能,同时又不改变其结构,是作为对现有类的一个包装。
一般来说,在代码设计中,我们应当遵循「多用组合,少用继承」的原则。通过装饰器模式动态地给一个对象添加一些额外的职责。就增加功能来说,装饰器模式相比生成子类更为灵活。
2.1 一个英雄联盟的例子
下班回去和朋友愉快地开黑,当我正在用亚索「面对疾风吧」的时候,突然想到,如果让我设计亚索英雄,我该怎么实现呢?
我灵光一闪,那肯定会先设计一个英雄的类。
然后,再实现一个 Yasuo 的类来继承这个
Hero
类。我还在想这个问题的时候,队友已经打了大龙,我的亚索身上就出现了大龙
buff
的印记。我突然想到,那该怎么给英雄增加大龙buff
呢?那增加个大龙buff
的属性不行吗?当然不太行,要知道,英雄联盟里面的大龙
buff
是会增加收益的。嗯,聪明的我已经想到办法了,再继承一次不就好了吗?
厉害了,但是如果亚索身上还有其他
buff
呢?毕竟LOL
里面是有红buff
、蓝buff
、大龙buff
等等存在,那岂不是有多少种就要增加多少个类吗?可以换种思路来思考这个问题,如果把
buff
当做我们身上的衣服。在不同的季节,我们会换上不同的衣服,到了冬天,甚至还会叠加多件衣服。当buff
消失了,就相当于把这件衣服脱了下来。如下图所示:衣服对人来说起到装饰的作用,
buff
对于亚索来说也只是增强效果。那么,你是不是有思路了呢? 没错,可以创建Buff
类,传入英雄类后获得一个新的增强后的英雄类。定义好所有的
buff
类之后,就可以直接套用到英雄身上,这样看起来是不是清爽了很多呢?这种写法看起来很像函数组合。3. ES7 装饰器
decorator
(装饰器)是 ES7 中的一个提案,目前处于stage-2
阶段,提案地址:JavaScript Decorators。装饰器与之前讲过的函数组合(
compose
)以及高阶函数很相似。装饰器使用@
作为标识符,被放置在被装饰代码前面。在其他语言中,早就已经有了比较成熟的装饰器方案。3.1 Python 中的装饰器
先来看一下 Python 中的一个装饰器的例子:
这个
auth
装饰器是通过检查cookie
来判断用户是否登录的。auth
函数是一个高阶函数,它接收了一个func
函数作为参数,返回了一个新的inner
函数。在
inner
函数中进行cookie
的检查,由此来判断是跳回登录页面还是继续执行func
函数。在所有需要权限验证的函数上,都可以使用这个
auth
装饰器,很简洁明了且无侵入。3.2 JavaScript 装饰器
JavaScript 中的装饰器和 Python 的装饰器类似,依赖于
Object.defineProperty
,一般是用来装饰类、类属性、类方法。使用装饰器可以做到不直接修改代码,就实现某些功能,做到真正的面向切面编程。这在一定程度上和
Proxy
很相似,但使用起来比Proxy
会更加简洁。3.3 类装饰器
装饰类的时候,装饰器方法一般会接收一个目标类作为参数。下面是一个给目标类增加静态属性
test
的例子:除了可以修改类本身,还可以通过修改原型,给实例增加新属性。下面是给目标类增加
speak
方法的例子:利用高阶函数的属性,还可以给装饰器传参,通过参数来判断对类进行什么处理。
如果你经常编写 react-redux 的代码,那么也会遇到需要将
store
中的数据映射到组件中的情况。connect
是一个高阶组件,它接收了两个函数mapStateToProps
和mapDispatchToProps
以及一个组件App
,最终返回了一个增强版的组件。有了装饰器之后,connect 的写法可以变得更加优雅。
3.4 类属性装饰器
类属性装饰器可以用在类的属性、方法、
get/set
函数中,一般会接收三个参数:Object.defineProperty
使用类属性装饰器可以做到很多有意思的事情,比如最开始举的那个
readonly
的例子:还可以用来统计一个函数的执行时间,以便于后期做一些性能优化。
在 react 知名的状态管理库 mobx 中,也通过装饰器来将类属性置为可观察属性,以此来实现响应式编程。
3.5 装饰器组合
如果你想要使用多个装饰器,那么该怎么办呢?装饰器是可以叠加的,根据离被装饰类/属性的距离来依次执行。
除此之外,在装饰器的提案中,还出现了一种组合了多种装饰器的装饰器例子。目前还没见到被使用。
通过使用
decorator
来声明一个组合装饰器xyz
,这个装饰器组合了多种装饰器。和下面这种写法是一样的。
4. 装饰器可以做哪些有意思的事情?
4.1 多重继承
在实现 JavaScript 多重继承的时候,可以使用
mixin
的方式,这里结合装饰器甚至还能更进一步简化mixin
的使用。mixin
方法将会接收一个父类列表,用其装饰目标类。我们理想中的用法应该是这样:和之前实现多重继承的时候实现原理一致,只需要拷贝父类的原型属性和实例属性就可以实现了。
这里创建了一个新的
Mixin
类,来将mixins
和targetClass
上面的所有属性都拷贝过去。我们来测试一下这个
mixin
方法是否能够正常工作吧。最终在浏览器中打印出来的
child
对象是这样的,证明了这个mixin
是可以正常工作的。也许你会问,为什么还要多创建一个多余的
Mixin
类呢?为什么不能直接修改targetClass
的constructor
呢?前面不是讲过Proxy
可以拦截constructor
吗?恭喜你,你已经想到了
Proxy
的一种使用场景。没错,这里用Proxy
的确会更加优雅。4.2 防抖和节流
以往我们在频繁触发的场景下,为了优化性能,经常会使用到节流函数。下面以 React 组件绑定滚动事件为例子:
在组件中绑定事件需要注意应当在组件销毁的时候进行解绑。而由于节流函数返回了一个新的匿名函数,所以为了之后能够有效解绑,不得不将这个匿名函数存起来,以便于之后使用。
但是在有了装饰器之后,我们就不必在每个绑定事件的地方都手动设置
throttle
方法,只需要在scroll
函数添加一个throttle
的装饰器就行了。使用起来比原来要简洁很多。
而实现防抖(
debounce
)函数装饰器和节流函数类似,这里也不再多说。如果对节流和防抖函数比较感兴趣,那么可以去阅读一下这篇文章:函数节流与函数防抖
4.3 数据格式验证
通过类属性装饰器来对类的属性进行类型的校验。
如果你觉得对属性一个个手动去校验太过麻烦,也可以通过编写校验规则,来对整个类进行校验。
4.4 core-decorators.js
core-decorators
是一个封装了常用装饰器的 JS 库,它归纳了下面这些装饰器(只列举了部分)。this
,告别箭头函数和bind
5. 总结
装饰器虽然还属于不稳定的语法,但在很多框架中都已经广泛使用,例如 Angular、Nestjs 等等,和 Java 中的注解用法非常相似。 装饰器在 TypeScript 中结合反射后还有一些更高级的应用,下篇文章会进行深入讲解。
推荐阅读: