1. 前言 知道装饰器还是在 Mobx 中见过类似的语法,对某个类或属性或方法进行包装修饰,是一种与类(class)相关的语法,用来注释或修改类和类方法 ,是实现面向切面编程(AOP)的一种重要模式。
1 2 3 4 5 6 7 8 9 10 11 12 import { Controller , Get , Post } from '@nestjs/common' ;@Controller ('cats' ); export class CatsController { @Post (); create (): string { return 'this action adds a new cat' ; } @Get (); findAll (): string { return 'this action returns all cats' ; } }
下面是一个使用装饰器修饰属性的简单例子,@readonly 可以将 count 属性设置为只读,通过此类方式,装饰器可以大大提高代码的简洁性和可读性。
1 2 3 class Person { @readonly count = 0 ; }
由于目前所有浏览器暂未支持装饰器语法,如果要看到运行效果,可以通过去 babel 官网进行验证。
2. 装饰器模式 经典的装饰器模式是一种结构型设计模式,它允许向一个现有的对象中添加新的功能,同时又保证不改变它的结构,是对现有类的一个包装修饰。
通常来说,在代码设计中,应该遵循 「多用组合,少用继承」的原则。通过装饰器模式可以动态地给一个对象添加额外的职责。就增加新的功能而言,装饰器模式比生成子类更加灵活,简单。
2.1 一个英雄的例子 在游戏中设计一个特定的英雄的类。
1 2 3 4 5 6 7 8 class Hero { attack ( ) {} } class SpecialHero extends Hero { attack ( ) { console .log ('斩钢闪' ); } }
当另外一个英雄具有上述英雄的一些技能(属性或方法)时,就需要第二次继承,此时需要继承 SpecialHero。
1 class FirstHero extends SpecialHero {}
如果有第二个英雄、第三个英雄时,就要继承四次 SpecialHero 类,岂不是有多少个英雄就要继承多少次 SpecialHero 类。
1 2 3 class SecondHero extends SpecialHero {}class ThirdHero extends SpecialHero {}...
可以换一种思路来思考这个问题,把英雄身上的 Skills 当做英雄身上的衣服。在不同的季节就换上不同的衣服,到了冬天,甚至会叠加多件衣服。当 Skills 都没有了,相当于把这件衣服脱了下来。
衣服对人来说起到修饰作用,Skills 对于英雄来说也只是增强效果。想到这里,你是不是有思路了呢?没错,可以创建 Skills 类,传入英雄后获得一个新的增强后的英雄类。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 class FirstHero extends Skills { construtor (hero ) { this .hero = hero; } extraDamage ( ) {} attack ( ) { return this .hero .attack () + this .extraDamage (); } } class SecondHero extends Skills { construtor (hero ) { this .hero = hero; } skillDecrese ( ) { return this .hero .skillDecrese () * 0.9 ; } } class ThirdHero extends Skills { construtor (hero ) { this .hero = hero; } backSpeed ( ) { return this .hero .backSpeed () * 0.5 ; } }
定义好所有的 Skills 类后,就可以直接套用到英雄身上,这样看起来是不是清爽了许多呢?这种写法看起来很像函数的组合。
1 2 3 4 const specialhero = new specialHero (); const firsthero = new FirstHero (specialhero); const secondhero = new FirstHero (specialhero); const thirdhero = new FirstHero (specialhero);
3. ES7 装饰器 decorator (装饰器)是 ES7 中的一个提案,目前处于 stage-2 阶段。装饰器与函数组合(compose)以及高阶函数很相似,使用 @ 符号作为标识符,放置在被装饰的代码前面。在 Python 语言中,早就已经有了非常成熟的装饰器方案,下面就来看看 Python 中的一个装饰器的例子。
3.1 Python 中的装饰器 1 2 3 4 5 6 7 8 9 10 11 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 会更加简洁。
3.3 类装饰器 装饰类的时候,装饰器方法一般会接收一个目标类作为参数。下面是一个给目标类增加静态属性 test 的例子:
1 2 3 4 5 6 const decoratorClass = (targetClass ) => { targetClass.test = "123" } @decoratorClass class Test {}Test .test ;
除了修改类本身,还可以通过修改原型,给实例增加新属性。下面是给目标类增加 speak 方法的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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 Studnent ('English' );student1.speak (); student2.speak ();
利用高阶函数的属性,还可以给装饰器传参,通过参数来判断对类进行什么处理。
1 2 3 4 5 6 7 const withLanguage = (language ) => (targetClass ) => { targetClass.prototype .language = language; } @withLanguage ('Chinese' ) class Student {}const student = new Student ();student.language ;
如果你经常编写 react-redux 的代码,会经常遇到需要将 store 中的数据映射到组件的情况。 其中 connect 就是一个高阶组件,它接收两个函数 mapStateToProps 和 mapDispatchToProps 以及一个组件 App , 最终返回一个增强版的组件。
1 2 class App extends React.Component {}connect (mapStateToProps,mapDispatchToProps)(App )
有了装饰器之后, connect 的写法可以变得更加优雅。
1 2 @connect (mapStateToProps,mapDispatchToProps) class App extends React.Component {}
3.4 类属性装饰器 类属性装饰器可以用在类的属性、方法、get/set 函数中,一般会接收三个参数;
(1) .target : 被修饰的类;
(2). name:类成员的名字;
(3). decriptor:属性描述符,对象会将这个参数传给 Object.defineProperty , 使用类属性可以做很多有趣的事情,比如最开始的那个 readonly
的例子:
1 2 3 4 5 6 7 8 9 function readonly (target,name,descriptor ) { descriptor.writable = false ; return descriptor; } class Person = { @readonly name = 'person' } const person = new Person ();person.name = 'tom' ;
还可以统计一个函数的执行时间,以便后期做一些性能优化。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 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 中,也通过装饰器将类属性设置为可观察属性,以此来实现响应式编程。
1 2 3 4 5 6 7 8 9 10 11 12 13 import { observale, action, autorun } from 'mobx' class Store { @observale count = 1 ; @action changeCount (count ) { this .count = count; } } const store = new Store ();autorun (() => { console .log ('count is ' , store.count ); }) store.changeCount (10 );
3.5 装饰器组合 如果需要多个装饰器,那该怎么办呢?装饰器是可以叠加的,根据与被装饰类或属性的距离,由近到远依次执行。(就近原则)
1 2 3 4 5 class Person { @time @log say ( ) {} }
除此之外在,在装饰器的提案中,还出现了一种组合多种装饰器的装饰器例子。目前还没有见到被使用。通过使用 decorato
r 来声明一个组合装饰器 xyz
, 这个装饰器组合了多种装饰器。
1 2 3 4 decorator @xyz (arg,arg2) { @foo @bar (arg) @baz (arg2) } @xyz (1 ,2 ) class C {}
上面的写法和下面的是类似的:
1 2 @foo @bar (1 ) @bar (2 ) class C {}
4.装饰器可以用来干啥 4.1 多重继承 通过 mixin
可以实现多重继承,如果使用装饰器可以进步一简化 mixin
的使用。 mixin
方法将会接收一个父类列表,通过它来装饰目标类。我们理想中的用法应该是这样:
1 2 @mixin (Parent1 ,Parent2 ,Parent3 ) class Childe {}
这里只需要拷贝父类的原型属性和实例属性就可以实现多重继承。这里创建了一个新的 Mixin
类,来将 mixin
和 targetClass
上面的所有属性都拷贝过去。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 const mixin = (...mixins ) => (targetClass ) => { mixins = [targetClass, ...mixins]; function copyProperties (target, source ) { for (let key of Reflect .ownKeys (source)) { if (key !== 'construtor' && 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 方法是否能够正常工作吧。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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 会更优雅。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 const mixin = (...mixins ) => (targetClass ) => { function copyProperties (target, source ) { for (let key of Reflect .ownKeys (source)) { if (key !== "construtor" && key !== "name" && key !== "prototype" ) { let desc = Object .getOwnProperyDescriptor (source, key); Object .defineProperty (target, key, desc); } } } for (let mixin of mixins) { copyProperties (targetClass, mixin); copyProperties (targetClass.prototype , mixin.prototype ); } 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 组件绑定滚动事件为例:
1 2 3 4 5 6 7 8 9 10 const App extends React .Component { componentDidMount ( ) { this .handleScroll = _.throttle (this .srcoll , 500 ); window .addEventListener ('srcoll' , this .handleScroll ) } componentWillUnmount ( ) { window .removeEventListener ('srcoll' , this .handleScroll ) } srcoll ( ) {} }
在组件中绑定事件需要注意在组件销毁时进行解绑。而由于节流函数返回了一个新的匿名函数,所以为了之后能够有效解绑,不得不将这个匿名函数存起来,以便之后使用。但是有了装饰器之后,我们就不必在每个绑定事件的地方都手动设置 throttle 方法,只需要在 scroll 函数添加 一个 throttle 的装饰器就行了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 const throttle = (wait ) => { 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 (); } } } } }
使用起来要比原来简介许多。
1 2 3 4 5 6 7 8 9 10 class App extends React.Component { componentDidMount ( ) { window .addEventListener ('srcoll' ,this .srcoll ); } componentWillUnmount ( ) { window .removeEventListener ('srcoll' ,this .srcoll ); } @throttle (50 ) srcoll ( ) {} }
而实现防抖 (debounce)函数装饰器和节流函数类似,这里不多说了。
1 2 3 4 5 6 7 8 9 10 11 12 const debounce = (wait ) => { let timer; return (target, name, descriptor ) => { const func = descriptor.value ; if (typeof func === 'function' ) { if (timer) clearTimeout (timer) timer = setTimeout (() => { fn.apply (this , args) }, wait); } } }
4.3 数据格式验证 通过类属性修饰器来对类的属性进行类型的校验。
1 2 3 4 5 6 7 8 9 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 } }
如果你觉得对属性一个一个手动去校验太过麻烦,也可以通过编写校验规则,来对整个类进行校验。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 const rules = { name : 'string' , password : 'string' , age : 'number' } const validator = rules => targetClass => { return new Proxy (target, { construct (target, args ) { const obj = new target (...args); for (let [name, type] of Object .entries (rules)) { if (typeof rules[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 库,它归纳了下面这些装饰器(只列举了部分)。
(1). autobind
:自动绑定 this , 告别箭头函数和 bind ;
(2). readonly
:将类属性设置为可读;
(3). override
:检查子类的方法是否覆盖了父类的同名方法 ;
(4). debounce
:防抖函数 ;
(5). throttle
:节流函数 ;
(6). enumerable
:让一个类方法变得可枚举;
(7). nonenumerable
:让一个类方法变得不可枚举 ;
(8). time
:打印函数执行耗时 ;
(9). mixin
:将多个对象混入类(和上面的 mixin 不太一样) ;
5.总结 JavaScript 装饰器是高级 JavaScript 编程语法中的一个较难的知识点,刚开始会理解不透彻,但随着在实际场景以及实际项目中开始使用,会是代码简洁许多,这里推荐尝试 React 的状态管理工具 Mobx 库,因为里面有多种使用装饰器的实际场景和案例。