俗话说,人靠衣装,佛靠金装。大街上的小姐姐都喜欢把本身打扮得美美的,让你忍不住多看几眼,这就是装饰的做用。html
装饰器是最新的 ECMA 中的一个提案,是一种与类(class
)相关的语法,用来注释或修改类和类方法。装饰器在 Python 和 Java 等语言中也被大量使用。装饰器是实现 AOP(面向切面)编程的一种重要方式。前端
下面是一个使用装饰器的简单例子,这个 @readonly
能够将 count
属性设置为只读。能够看出来,装饰器大大提升了代码的简洁性和可读性。react
class Person {
@readonly count = 0;
}
复制代码
因为浏览器还未支持装饰器,为了让你们可以正常看到效果,这里我使用 Parcel 进行了一下简单的配置,能够去 clone 这个仓库后再来运行本文涉及到的全部例子,仓库地址:learn es6git
本文涉及到 Object.defineProperty、高阶函数等知识,若是以前没有了解过相关概念,建议先了解后再来阅读本文。es6
在开始讲解装饰器以前,先从经典的装饰器模式提及。装饰器模式是一种结构型设计模式,它容许向一个现有的对象添加新的功能,同时又不改变其结构,是做为对现有类的一个包装。github
通常来讲,在代码设计中,咱们应当遵循「多用组合,少用继承」的原则。经过装饰器模式动态地给一个对象添加一些额外的职责。就增长功能来讲,装饰器模式相比生成子类更为灵活。面试
下班回去和朋友愉快地开黑,当我正在用亚索「面对疾风吧」的时候,忽然想到,若是让我设计亚索英雄,我该怎么实现呢?编程
我灵光一闪,那确定会先设计一个英雄的类。redux
class Hero {
attack() {}
}
复制代码
而后,再实现一个 Yasuo 的类来继承这个 Hero
类。设计模式
class Yasuo extends Hero {
attack() {
console.log("斩钢闪");
}
}
复制代码
我还在想这个问题的时候,队友已经打了大龙,个人亚索身上就出现了大龙 buff
的印记。我忽然想到,那该怎么给英雄增长大龙 buff
呢?那增长个大龙 buff
的属性不行吗?
固然不太行,要知道,英雄联盟里面的大龙 buff
是会增长收益的。
嗯,聪明的我已经想到办法了,再继承一次不就行了吗?
class BaronYasuo extends Yasuo {}
复制代码
厉害了,可是若是亚索身上还有其余 buff
呢?毕竟 LOL
里面是有红 buff
、蓝 buff
、大龙 buff
等等存在,那岂不是有多少种就要增长多少个类吗?
能够换种思路来思考这个问题,若是把 buff
当作咱们身上的衣服。在不一样的季节,咱们会换上不一样的衣服,到了冬天,甚至还会叠加多件衣服。当 buff
消失了,就至关于把这件衣服脱了下来。以下图所示:
衣服对人来讲起到装饰的做用,buff
对于亚索来讲也只是加强效果。那么,你是否是有思路了呢? 没错,能够建立 Buff
类,传入英雄类后得到一个新的加强后的英雄类。
class RedBuff extends Buff {
constructor(hero) {
this.hero = hero;
}
// 红buff形成额外伤害
extraDamage() {
}
attack() {
return this.hero.attack() + this.extraDamage();
}
}
class BlueBuff extends Buff {
constructor(hero) {
this.hero = hero;
}
// 技能CD(减10%)
CDR() {
return this.hero.CDR() * 0.9;
}
}
class BaronBuff extends Buff {
constructor(hero) {
this.hero = hero;
}
// 回城速度缩短一半
backSpeed() {
return this.hero.backSpeed * 0.5;
}
}
复制代码
定义好全部的 buff
类以后,就能够直接套用到英雄身上,这样看起来是否是清爽了不少呢?这种写法看起来很像函数组合。
const yasuo = new Yasuo();
const redYasuo = new RedBuff(yasuo); // 红buff亚索
const blueYasuo = new BlueBuff(yasuo); // 蓝buff亚索
const redBlueYasuo = new BlueBuff(redYasuo); // 红蓝buff亚索
复制代码
decorator
(装饰器)是 ES7 中的一个提案,目前处于 stage-2
阶段,提案地址:JavaScript Decorators。
装饰器与以前讲过的函数组合(compose
)以及高阶函数很类似。装饰器使用 @
做为标识符,被放置在被装饰代码前面。在其余语言中,早就已经有了比较成熟的装饰器方案。
先来看一下 Python 中的一个装饰器的例子:
def auth(func):
def inner(request,*args,**kwargs):
v = request.COOKIES.get('user')
if not v:
return redirect('/login')
return func(request, *args,**kwargs)
return inner
@auth
def index(request):
v = request.COOKIES.get("user")
return render(request,"index.html",{"current_user":v})
复制代码
这个 auth
装饰器是经过检查 cookie
来判断用户是否登陆的。auth
函数是一个高阶函数,它接收了一个 func
函数做为参数,返回了一个新的 inner
函数。
在 inner
函数中进行 cookie
的检查,由此来判断是跳回登陆页面仍是继续执行 func
函数。
在全部须要权限验证的函数上,均可以使用这个 auth
装饰器,很简洁明了且无侵入。
JavaScript 中的装饰器和 Python 的装饰器相似,依赖于 Object.defineProperty
,通常是用来装饰类、类属性、类方法。
使用装饰器能够作到不直接修改代码,就实现某些功能,作到真正的面向切面编程。这在必定程度上和 Proxy
很类似,但使用起来比 Proxy
会更加简洁。
注意:装饰器目前还处于 stage-2,意味着语法以后也许会有变更。装饰器用于函数、对象等等已经有一些规划,请看:Future built-in decorators
装饰类的时候,装饰器方法通常会接收一个目标类做为参数。下面是一个给目标类增长静态属性 test
的例子:
const decoratorClass = (targetClass) => {
targetClass.test = '123'
}
@decoratorClass
class Test {}
Test.test; // '123'
复制代码
除了能够修改类自己,还能够经过修改原型,给实例增长新属性。下面是给目标类增长 speak
方法的例子:
const withSpeak = (targetClass) => {
const prototype = targetClass.prototype;
prototype.speak = function() {
console.log('I can speak ', this.language);
}
}
@withSpeak
class Student {
constructor(language) {
this.language = language;
}
}
const student1 = new Student('Chinese');
const student2 = new Student('English');
student1.speak(); // I can speak Chinese
student2.speak(); // I can speak Chinese
复制代码
利用高阶函数的属性,还能够给装饰器传参,经过参数来判断对类进行什么处理。
const withLanguage = (language) => (targetClass) => {
targetClass.prototype.language = language;
}
@withLanguage('Chinese')
class Student {
}
const student = new Student();
student.language; // 'Chinese'
复制代码
若是你常常编写 react-redux 的代码,那么也会遇到须要将 store
中的数据映射到组件中的状况。connect
是一个高阶组件,它接收了两个函数 mapStateToProps
和 mapDispatchToProps
以及一个组件 App
,最终返回了一个加强版的组件。
class App extends React.Component {
}
connect(mapStateToProps, mapDispatchToProps)(App)
复制代码
有了装饰器以后,connect 的写法能够变得更加优雅。
@connect(mapStateToProps, mapDispatchToProps)
class App extends React.Component {
}
复制代码
类属性装饰器能够用在类的属性、方法、get/set
函数中,通常会接收三个参数:
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();
复制代码
在 react 知名的状态管理库 mobx 中,也经过装饰器来将类属性置为可观察属性,以此来实现响应式编程。
import {
observable,
action,
autorun
} from 'mobx'
class Store {
@observable count = 1;
@action
changeCount(count) {
this.count = count;
}
}
const store = new Store();
autorun(() => {
console.log('count is ', store.count);
})
store.changeCount(10); // 修改 count 的值,会引发 autorun 中的函数自动执行。
复制代码
若是你想要使用多个装饰器,那么该怎么办呢?装饰器是能够叠加的,根据离被装饰类/属性的距离来依次执行。
class Person {
@time
@log
say() {}
}
复制代码
除此以外,在装饰器的提案中,还出现了一种组合了多种装饰器的装饰器例子。目前还没见到被使用。
经过使用 decorator
来声明一个组合装饰器 xyz
,这个装饰器组合了多种装饰器。
decorator @xyz(arg, arg2 {
@foo @bar(arg) @baz(arg2)
}
@xyz(1, 2) class C { }
复制代码
和下面这种写法是同样的。
@foo @bar(1) @baz(2)
class C { }
复制代码
在实现 JavaScript 多重继承的时候,可使用 mixin
的方式,这里结合装饰器甚至还能更进一步简化 mixin
的使用。
mixin
方法将会接收一个父类列表,用其装饰目标类。咱们理想中的用法应该是这样:
@mixin(Parent1, Parent2, Parent3)
class Child {}
复制代码
和以前实现多重继承的时候实现原理一致,只须要拷贝父类的原型属性和实例属性就能够实现了。
这里建立了一个新的 Mixin
类,来将 mixins
和 targetClass
上面的全部属性都拷贝过去。
const mixin = (...mixins) => (targetClass) => {
mixins = [targetClass, ...mixins];
function copyProperties(target, source) {
for (let key of Reflect.ownKeys(source)) {
if (key !== 'constructor'
&& key !== 'prototype'
&& key !== 'name'
) {
let desc = Object.getOwnPropertyDescriptor(source, key);
Object.defineProperty(target, key, desc);
}
}
}
class Mixin {
constructor(...args) {
for (let mixin of mixins) {
copyProperties(this, new mixin(...args)); // 拷贝实例属性
}
}
}
for (let mixin of mixins) {
copyProperties(Mixin, mixin); // 拷贝静态属性
copyProperties(Mixin.prototype, mixin.prototype); // 拷贝原型属性
}
return Mixin;
}
export default mixin
复制代码
咱们来测试一下这个 mixin
方法是否可以正常工做吧。
class Parent1 {
p1() {
console.log('this is parent1')
}
}
class Parent2 {
p2() {
console.log('this is parent2')
}
}
class Parent3 {
p3() {
console.log('this is parent3')
}
}
@mixin(Parent1, Parent2, Parent3)
class Child {
c1 = () => {
console.log('this is child')
}
}
const child = new Child();
console.log(child);
复制代码
最终在浏览器中打印出来的 child
对象是这样的,证实了这个 mixin
是能够正常工做的。
注意:这里的
Child
类就是前面的Mixin
类。
也许你会问,为何还要多建立一个多余的 Mixin
类呢?为何不能直接修改 targetClass
的 constructor
呢?前面不是讲过 Proxy
能够拦截 constructor
吗?
恭喜你,你已经想到了 Proxy
的一种使用场景。没错,这里用 Proxy
的确会更加优雅。
const mixin = (...mixins) => (targetClass) => {
function copyProperties(target, source) {
for (let key of Reflect.ownKeys(source)) {
if ( key !== 'constructor'
&& key !== 'prototype'
&& key !== 'name'
) {
let desc = Object.getOwnPropertyDescriptor(source, key);
Object.defineProperty(target, key, desc);
}
}
}
for (let mixin of mixins) {
copyProperties(targetClass, mixin); // 拷贝静态属性
copyProperties(targetClass.prototype, mixin.prototype); // 拷贝原型属性
}
// 拦截 construct 方法,进行实例属性的拷贝
return new Proxy(targetClass, {
construct(target, args) {
const obj = new target(...args);
for (let mixin of mixins) {
copyProperties(obj, new mixin()); // 拷贝实例属性
}
return obj;
}
});
}
复制代码
以往咱们在频繁触发的场景下,为了优化性能,常常会使用到节流函数。下面以 React 组件绑定滚动事件为例子:
class App extends React.Component {
componentDidMount() {
this.handleScroll = _.throttle(this.scroll, 500);
window.addEveneListener('scroll', this.handleScroll);
}
componentWillUnmount() {
window.removeEveneListener('scroll', this.handleScroll);
}
scroll() {}
}
复制代码
在组件中绑定事件须要注意应当在组件销毁的时候进行解绑。而因为节流函数返回了一个新的匿名函数,因此为了以后可以有效解绑,不得不将这个匿名函数存起来,以便于以后使用。
可是在有了装饰器以后,咱们就没必要在每一个绑定事件的地方都手动设置 throttle
方法,只须要在 scroll
函数添加一个 throttle
的装饰器就好了。
const throttle = (time) => {
let prev = new Date();
return (target, name, descriptor) => {
const func = descriptor.value;
if (typeof func === 'function') {
descriptor.value = function(...args) {
const now = new Date();
if (now - prev > wait) {
fn.apply(this, args);
prev = new Date();
}
}
}
}
}
复制代码
使用起来比原来要简洁不少。
class App extends React.Component {
componentDidMount() {
window.addEveneListener('scroll', this.scroll);
}
componentWillUnmount() {
window.removeEveneListener('scroll', this.scroll);
}
@throttle(50)
scroll() {}
}
复制代码
而实现防抖(debounce
)函数装饰器和节流函数相似,这里也再也不多说。
const debounce = (time) => {
let timer;
return (target, name, descriptor) => {
const func = descriptor.value;
if (typeof func === 'function') {
descriptor.value = function(...args) {
if(timer) clearTimeout(timer)
timer = setTimeout(()=> {
fn.apply(this, args)
}, wait)
}
}
}
}
复制代码
若是对节流和防抖函数比较感兴趣,那么能够去阅读一下这篇文章:函数节流与函数防抖
经过类属性装饰器来对类的属性进行类型的校验。
const validate = (type) => (target, name) => {
if (typeof target[name] !== type) {
throw new Error(`attribute ${name} must be ${type} type`)
}
}
class Form {
@validate('string')
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();
复制代码
core-decorators
是一个封装了经常使用装饰器的 JS 库,它概括了下面这些装饰器(只列举了部分)。
this
,告别箭头函数和 bind
装饰器虽然还属于不稳定的语法,但在不少框架中都已经普遍使用,例如 Angular、Nestjs 等等,和 Java 中的注解用法很是类似。 装饰器在 TypeScript 中结合反射后还有一些更高级的应用,下篇文章会进行深刻讲解。
若是你以为这篇内容对你挺有启发,我想邀请你帮我三个小忙: