内容简介:俗话说,人靠衣装,佛靠金装。大街上的小姐姐都喜欢把自己打扮得美美的,让你忍不住多看几眼,这就是装饰的作用。装饰器是最新的 ECMA 中的一个提案,是一种与类(class)相关的语法,用来注释或修改类和类方法。装饰器在 Python 和 Java 等语言中也被大量使用。装饰器是实现 AOP(面向切面)编程的一种重要方式。
俗话说,人靠衣装,佛靠金装。大街上的小姐姐都喜欢把自己打扮得美美的,让你忍不住多看几眼,这就是装饰的作用。
1. 前言
装饰器是最新的 ECMA 中的一个提案,是一种与类(class)相关的语法,用来注释或修改类和类方法。装饰器在 Python 和 Java 等语言中也被大量使用。装饰器是实现 AOP(面向切面)编程的一种重要方式。
下面是一个使用装饰器的简单例子,这个 @readonly
可以将 count
属性设置为只读。可以看出来,装饰器大大提高了代码的简洁性和可读性。
class Person { @readonly count = 0; }
由于浏览器还未支持装饰器,为了让大家能够正常看到效果,这里我使用 Parcel 进行了一下简单的配置,可以去 clone 这个仓库后再来运行本文涉及到的所有例子,仓库地址:[learn es6][https://github.com/yinguangyao/ES6]
本文涉及到 [Object.defineProperty][https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty]、高阶函数等知识,如果之前没有了解过相关概念,建议先了解后再来阅读本文。
2. 装饰器模式
在开始讲解装饰器之前,先从经典的装饰器模式说起。装饰器模式是一种结构型设计模式,它允许向一个现有的对象添加新的功能,同时又不改变其结构,是作为对现有类的一个包装。一般来说,在代码设计中,我们应当遵循「多用组合,少用继承」的原则。通过装饰器模式动态地给一个对象添加一些额外的职责。就增加功能来说,装饰器模式相比生成子类更为灵活。
2.1 一个英雄联盟的例子
下班回去和朋友愉快地开黑,当我正在用亚索「面对疾风吧」的时候,突然想到,如果让我设计亚索英雄,我该怎么实现呢?
我想了想,肯定会先设计一个英雄的类。
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亚索
3. ES7 装饰器
decorator(装饰器)是 ES7 中的一个提案,目前处于 stage-2 阶段,提案地址:[JavaScript Decorators][https://github.com/tc39/proposal-decorators]。装饰器与之前讲过的函数组合(compose)以及高阶函数很相似。装饰器使用 @
作为标识符,被放置在被装饰代码前面。在其他语言中,早就已经有了比较成熟的装饰器方案。
3.1 Python 中的装饰器
先来看一下 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
装饰器,很简洁明了且无侵入。
3.2 JavaScript 装饰器
JavaScript 中的装饰器和 Python 的装饰器类似,依赖于 Object.defineProperty
,一般是用来装饰类、类属性、类方法。使用装饰器可以做到不直接修改代码,就实现某些功能,做到真正的面向切面编程。这在一定程度上和 Proxy 很相似,但使用起来比 Proxy 会更加简洁。
注意:装饰器目前还处于 stage-2,意味着语法之后也许会有变动。装饰器用于函数、对象等等已经有一些规划,请看:[Future built-in decorators][https://github.com/tc39/proposal-decorators/blob/master/NEXTBUILTINS.md#applying-built-in-decorators-to-other-syntactic-forms]
3.3 类装饰器
装饰类的时候,装饰器方法一般会接收一个目标类作为参数。下面是一个给目标类增加静态属性 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 { }
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();
在 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 中的函数自动执行。
3.5 装饰器组合
如果你想要使用多个装饰器,那么该怎么办呢?装饰器是可以叠加的,根据离被装饰类/属性的距离来依次执行。
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 { }
4. 装饰器可以做哪些有意思的事情?
4.1 多重继承
之前我们在讲解 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; } }); }
4.2 防抖和节流
以往我们在频繁触发的场景下,为了优化性能,经常会使用到节流函数。下面以 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) } } } }
如果对节流和防抖函数比较感兴趣,那么可以去阅读一下这篇文章:[函数节流与函数防抖][https://juejin.im/entry/58c0379e44d9040068dc952f]
4.3 数据格式验证
通过类属性装饰器来对类的属性进行类型的校验。
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();
4.4 core-decorators.js
core-decorators 是一个封装了常用装饰器的 JS 库,它归纳了下面这些装饰器(只列举了部分)。
-
autobind :自动绑定
this
,告别箭头函数和bind
-
readonly :将类属性设置为只读
-
override :检查子类的方法是否正确覆盖了父类的同名方法
-
debounce :防抖函数
-
throttle :节流函数
-
enumerable :让一个类方法变得可枚举
-
nonenumerable :让一个类属性变得不可枚举
-
time :打印函数执行耗时
-
mixin :将多个对象混入类(和我们上面的 mixin 不太一样)
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- 装饰器与元数据反射(1)方法装饰器
- 草根学Python(十六) 装饰器(逐步演化成装饰器)
- 装饰器与元数据反射(2)属与类性装饰器
- Python装饰器
- python--装饰器详解
- 030. Python装饰器
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。