一文读懂 JS 装饰器,这是一个会打扮的装饰器

开发 前端
俗话说,人靠衣装,佛靠金装。大街上的小姐姐都喜欢把自己打扮得美美的,让你忍不住多看几眼,这就是装饰的作用。

俗话说,人靠衣装,佛靠金装。大街上的小姐姐都喜欢把自己打扮得美美的,让你忍不住多看几眼,这就是装饰的作用。

1. 前言

装饰器是最新的 ECMA 中的一个提案,是一种与类(class)相关的语法,用来注释或修改类和类方法。装饰器在 Python 和 Java 等语言中也被大量使用。装饰器是实现 AOP(面向切面)编程的一种重要方式。

下面是一个使用装饰器的简单例子,这个 @readonly 可以将 count 属性设置为只读。可以看出来,装饰器大大提高了代码的简洁性和可读性。 

  1. class Person {  
  2.     @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 一个英雄联盟的例子

下班回去和朋友愉快地开黑,当我正在用亚索「面对疾风吧」的时候,突然想到,如果让我设计亚索英雄,我该怎么实现呢?

[[322000]]

我想了想,肯定会先设计一个英雄的类。 

  1. class Hero {  
  2.     attack() {}  

然后,再实现一个 Yasuo 的类来继承这个 Hero 类。 

  1. class Yasuo extends Hero {  
  2.     attack() {  
  3.         console.log("斩钢闪");  
  4.     }  

我还在想这个问题的时候,队友已经打了大龙,我的亚索身上就出现了大龙 buff 的印记。我突然想到,那该怎么给英雄增加大龙 buff 呢?那增加个大龙 buff 的属性不行吗?当然不太行,要知道,英雄联盟里面的大龙 buff 是会增加收益的。嗯,聪明的我已经想到办法了,再继承一次不就好了吗? 

  1. class BaronYasuo extends Yasuo {  

厉害了,但是如果亚索身上还有其他 buff 呢?毕竟 LOL 里面是有红 buff、蓝 buff、大龙 buff等等存在,那岂不是有多少种就要增加多少个类吗?

可以换种思路来思考这个问题,如果把 buff 当做我们身上的衣服。在不同的季节,我们会换上不同的衣服,到了冬天,甚至还会叠加多件衣服。当 buff 消失了,就相当于把这件衣服脱了下来。如下图所示:

衣服对人来说起到装饰的作用,buff 对于亚索来说也只是增强效果。那么,你是不是有思路了呢?没错,可以创建 Buff 类,传入英雄类后获得一个新的增强后的英雄类。 

  1. class RedBuff extends Buff {  
  2.     constructor(hero) {  
  3.         this.hero = hero;  
  4.     }  
  5.     // 红buff造成额外伤害  
  6.     extraDamage() {  
  7.     } 
  8.      attack() {  
  9.         returnthis.hero.attack() + this.extraDamage();  
  10.     }  
  11.  
  12. class BlueBuff extends Buff {  
  13.     constructor(hero) {  
  14.         this.hero = hero;  
  15.     }  
  16.     // 技能CD(减10%)  
  17.     CDR() {  
  18.         returnthis.hero.CDR() * 0.9;  
  19.     }  
  20.  
  21. class BaronBuff extends Buff {  
  22.     constructor(hero) {  
  23.         this.hero = hero;  
  24.     }  
  25.     // 回城速度缩短一半  
  26.     backSpeed() {  
  27.         returnthis.hero.backSpeed * 0.5;  
  28.     }  

定义好所有的 buff 类之后,就可以直接套用到英雄身上,这样看起来是不是清爽了很多呢?这种写法看起来很像函数组合。 

  1. const yasuo = new Yasuo();  
  2. const redYasuo = new RedBuff(yasuo); // 红buff亚索  
  3. const blueYasuo = new BlueBuff(yasuo); // 蓝buff亚索  
  4. const redBlueYasuo = new BlueBuff(redYasuo); // 红蓝buff亚索 

[[322003]]

3. ES7 装饰器

decorator(装饰器)是 ES7 中的一个提案,目前处于 stage-2 阶段,提案地址:[JavaScript Decorators][https://github.com/tc39/proposal-decorators]。装饰器与之前讲过的函数组合(compose)以及高阶函数很相似。装饰器使用 @ 作为标识符,被放置在被装饰代码前面。在其他语言中,早就已经有了比较成熟的装饰器方案。

3.1 Python 中的装饰器

先来看一下 Python 中的一个装饰器的例子: 

  1. def auth(func):  
  2.     def inner(request,*args,**kwargs):  
  3.         v = request.COOKIES.get('user')  
  4.         if not v:  
  5.             return redirect('/login')  
  6.         return func(request, *args,**kwargs)  
  7.     return inner  
  8. @auth  
  9. def index(request):  
  10.     v = request.COOKIES.get("user")  
  11.     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 的例子: 

  1. const decoratorClass = (targetClass) => {  
  2.     targetClass.test = '123'  
  3.  
  4. @decoratorClass  
  5. class Test {}  
  6. Test.test; // '123' 

除了可以修改类本身,还可以通过修改原型,给实例增加新属性。下面是给目标类增加 speak 方法的例子: 

  1. const withSpeak = (targetClass) => {  
  2.     const prototype = targetClass.prototype;  
  3.     prototype.speak = function() {  
  4.         console.log('I can speak ', this.language);  
  5.     }  
  6.  
  7. @withSpeak  
  8. class Student {  
  9.     constructor(language) {  
  10.         this.language = language;  
  11.     } 
  12.   
  13. const student1 = new Student('Chinese');  
  14. const student2 = new Student('English');  
  15. student1.speak(); // I can speak  Chinese  
  16. student2.speak(); // I can speak  Chinese 

利用高阶函数的属性,还可以给装饰器传参,通过参数来判断对类进行什么处理。 

  1. const withLanguage = (language) =>(targetClass) => {  
  2.     targetClass.prototype.language = language;  
  3.  
  4. @withLanguage('Chinese')  
  5. class Student {  
  6.  
  7. const student = new Student();  
  8. student.language; // 'Chinese' 

如果你经常编写 react-redux 的代码,那么也会遇到需要将 store 中的数据映射到组件中的情况。connect 是一个高阶组件,它接收了两个函数 mapStateToProps 和 mapDispatchToProps 以及一个组件 App,最终返回了一个增强版的组件。 

  1. class App extends React.Component {  
  2.  
  3. connect(mapStateToProps, mapDispatchToProps)(App) 

有了装饰器之后,connect 的写法可以变得更加优雅。 

  1. @connect(mapStateToProps, mapDispatchToProps)  
  2. class App extends React.Component {  

3.4 类属性装饰器

类属性装饰器可以用在类的属性、方法、get/set 函数中,一般会接收三个参数:

  1.  target:被修饰的类
  2.  name:类成员的名字
  3.  descriptor:属性描述符,对象会将这个参数传给 Object.defineProperty使用类属性装饰器可以做到很多有意思的事情,比如最开始举的那个 readonly 的例子: 
  1. function readonly(target, name, descriptor) {  
  2.   descriptor.writable = false 
  3.   return descriptor;  
  4.  
  5. class Person {  
  6.     @readonly name = 'person'  
  7.  
  8. const person = new Person();  
  9. person.name = 'tom'

还可以用来统计一个函数的执行时间,以便于后期做一些性能优化。 

  1. function time(target, name, descriptor) {  
  2.     const func = descriptor.value;  
  3.     if (typeof func === 'function') {  
  4.         descriptor.value = function(...args) {  
  5.             console.time();  
  6.             const results = func.apply(this, args);  
  7.             console.timeEnd();  
  8.             return results;  
  9.         }  
  10.     }  
  11.  
  12. class Person {  
  13.     @time  
  14.     say() {  
  15.         console.log('hello')  
  16.     }  
  17.  
  18. const person = new Person();  
  19. person.say(); 

在 react 知名的状态管理库 mobx 中,也通过装饰器来将类属性置为可观察属性,以此来实现响应式编程。 

  1. import {  
  2.     observable,  
  3.     action,  
  4.     autorun  
  5. } from 'mobx'  
  6. class Store {  
  7.     @observable count = 1 
  8.     @action  
  9.     changeCount(count) {  
  10.         this.count = count;  
  11.     }  
  12.  
  13. const store = new Store();  
  14. autorun(() => {  
  15.     console.log('count is ', store.count);  
  16. }) 
  17.  store.changeCount(10); // 修改 count 的值,会引起 autorun 中的函数自动执行。 

3.5 装饰器组合

如果你想要使用多个装饰器,那么该怎么办呢?装饰器是可以叠加的,根据离被装饰类/属性的距离来依次执行。 

  1. class Person {  
  2.     @time  
  3.     @log  
  4.     say() {}  

除此之外,在装饰器的提案中,还出现了一种组合了多种装饰器的装饰器例子。目前还没见到被使用。通过使用 decorator 来声明一个组合装饰器 xyz,这个装饰器组合了多种装饰器。 

  1. decorator @xyz(arg, arg2 {  
  2.   @foo @bar(arg) @baz(arg2)  
  3.  
  4. @xyz(1, 2) class C { } 

和下面这种写法是一样的。 

  1. @foo @bar(1) @baz(2) 
  2.  class C { } 

4. 装饰器可以做哪些有意思的事情?

4.1 多重继承

之前我们在讲解 JavaScript 多重继承的时候,使用过 mixin 的方式,这里结合装饰器甚至还能更进一步简化 mixin 的使用。mixin 方法将会接收一个父类列表,用其装饰目标类。我们理想中的用法应该是这样: 

  1. @mixin(Parent1, Parent2, Parent3)  
  2. class Child {} 

和之前实现多重继承的时候实现原理一致,只需要拷贝父类的原型属性和实例属性就可以实现了。这里创建了一个新的 Mixin 类,来将 mixins 和 targetClass 上面的所有属性都拷贝过去。 

  1. const mixin = (...mixins) => (targetClass) => {  
  2.   mixins = [targetClass, ...mixins];  
  3.   function copyProperties(target, source) {  
  4.     for (let key of Reflect.ownKeys(source)) {  
  5.       if (key !== 'constructor'  
  6.         && key !== 'prototype'  
  7.         && key !== 'name'  
  8.       ) {  
  9.         let desc = Object.getOwnPropertyDescriptor(source, key);  
  10.         Object.defineProperty(target, key, desc);  
  11.       }  
  12.     }  
  13.   }  
  14.   class Mixin {  
  15.     constructor(...args) {  
  16.       for (let mixin of mixins) {  
  17.         copyProperties(this, new mixin(...args)); // 拷贝实例属性  
  18.       }  
  19.     }  
  20.   }  
  21.   for (let mixin of mixins) {  
  22.     copyProperties(Mixin, mixin); // 拷贝静态属性  
  23.     copyProperties(Mixin.prototype, mixin.prototype); // 拷贝原型属性  
  24.   }  
  25.   return Mixin;  
  26.  
  27. export default mixin 

我们来测试一下这个 mixin 方法是否能够正常工作吧。 

  1. class Parent1 {  
  2.     p1() {  
  3.         console.log('this is parent1')  
  4.     }  
  5.  
  6. class Parent2 {  
  7.     p2() {  
  8.         console.log('this is parent2')  
  9.     }  
  10.  
  11. class Parent3 {  
  12.     p3() {  
  13.         console.log('this is parent3')  
  14.     }  
  15.  
  16. @mixin(Parent1, Parent2, Parent3)  
  17. class Child {  
  18.     c1 = () => {  
  19.         console.log('this is child')  
  20.     }  
  21.  
  22. const child = new Child();  
  23. console.log(child); 

最终在浏览器中打印出来的 child 对象是这样的,证明了这个 mixin 是可以正常工作的。

注意:这里的 Child 类就是前面的 Mixin 类。

image.png-69.4kB

也许你会问,为什么还要多创建一个多余的 Mixin 类呢?为什么不能直接修改 targetClass 的 constructor 呢?前面不是讲过 Proxy 可以拦截 constructor 吗?恭喜你,你已经想到了 Proxy 的一种使用场景。没错,这里用 Proxy 的确会更加优雅。 

  1. const mixin = (...mixins) =>(targetClass) => {  
  2.     function copyProperties(target, source) {  
  3.         for (let key ofReflect.ownKeys(source)) {  
  4.           if ( key !== 'constructor'  
  5.             && key !== 'prototype'  
  6.             && key !== 'name'  
  7.           ) {  
  8.             let desc = Object.getOwnPropertyDescriptor(source, key);  
  9.             Object.defineProperty(target, key, desc);  
  10.           }  
  11.         }  
  12.       }    
  13.       for (let mixin of mixins) {  
  14.         copyProperties(targetClass, mixin); // 拷贝静态属性  
  15.         copyProperties(targetClass.prototype, mixin.prototype); // 拷贝原型属性  
  16.       }  
  17.       // 拦截 construct 方法,进行实例属性的拷贝  
  18.       returnnewProxy(targetClass, {  
  19.         construct(target, args) {  
  20.           const obj = new target(...args);  
  21.           for (let mixin of mixins) {  
  22.               copyProperties(obj, new mixin()); // 拷贝实例属性  
  23.           }  
  24.           return obj;  
  25.         }  
  26.       });  

4.2 防抖和节流

以往我们在频繁触发的场景下,为了优化性能,经常会使用到节流函数。下面以 React 组件绑定滚动事件为例子: 

  1. class App extends React.Component {  
  2.     componentDidMount() {  
  3.         this.handleScroll = _.throttle(this.scroll, 500);  
  4.         window.addEveneListener('scroll', this.handleScroll);  
  5.     }  
  6.     componentWillUnmount() {  
  7.         window.removeEveneListener('scroll', this.handleScroll);  
  8.     }  
  9.     scroll() {}  

在组件中绑定事件需要注意应当在组件销毁的时候进行解绑。而由于节流函数返回了一个新的匿名函数,所以为了之后能够有效解绑,不得不将这个匿名函数存起来,以便于之后使用。但是在有了装饰器之后,我们就不必在每个绑定事件的地方都手动设置 throttle 方法,只需要在 scroll 函数添加一个 throttle 的装饰器就行了。 

  1. const throttle = (time) => {  
  2.     let prev = newDate();  
  3.     return(target, name, descriptor) => {  
  4.         const func = descriptor.value;  
  5.         if (typeof func === 'function') {  
  6.             descriptor.value = function(...args) {  
  7.                 const now = newDate();  
  8.                 if (now - prev > wait) {  
  9.                     fn.apply(this, args);  
  10.                     prev = newDate();  
  11.                 }  
  12.             }  
  13.         }  
  14.     }  

使用起来比原来要简洁很多。 

  1. class App extends React.Component {  
  2.     componentDidMount() {  
  3.         window.addEveneListener('scroll', this.scroll);  
  4.     }  
  5.     componentWillUnmount() {  
  6.         window.removeEveneListener('scroll', this.scroll);  
  7.     }  
  8.     @throttle(50)  
  9.     scroll() {}  

而实现防抖(debounce)函数装饰器和节流函数类似,这里也不再多说。

 

  1. const debounce = (time) => {  
  2.     let timer;  
  3.     return(target, name, descriptor) => {  
  4.         const func = descriptor.value;  
  5.         if (typeof func === 'function') {  
  6.             descriptor.value = function(...args) {  
  7.                 if(timer) clearTimeout(timer)  
  8.                 timer = setTimeout(()=> {  
  9.                     fn.apply(this, args)  
  10.                 }, wait)  
  11.             }  
  12.         }  
  13.     }  

如果对节流和防抖函数比较感兴趣,那么可以去阅读一下这篇文章:[函数节流与函数防抖][https://juejin.im/entry/58c0379e44d9040068dc952f]

4.3 数据格式验证

通过类属性装饰器来对类的属性进行类型的校验。 

  1. const validate = (type) =>(target, name) => {  
  2.     if (typeof target[name] !== type) {  
  3.         thrownewError(`attribute ${name} must be ${type} type`)  
  4.     }  
  5.  
  6. class Form {  
  7.     @validate('string')  
  8.     name = 111// Error: attribute name must be ${type} type  

如果你觉得对属性一个个手动去校验太过麻烦,也可以通过编写校验规则,来对整个类进行校验。

 

  1. const rules = {  
  2.     name: 'string',  
  3.     password: 'string',  
  4.     age: 'number'  
  5.  
  6. const validator = rules =>targetClass => {  
  7.     returnnewProxy(targetClass, {  
  8.         construct(target, args) {  
  9.             const obj = new target(...args);  
  10.             for (let [name, type] ofObject.entries(rules)) {  
  11.                 if (typeof obj[name] !== type) {  
  12.                     thrownewError(`${name} must be ${type}`)  
  13.                 }  
  14.             }  
  15.             return obj;  
  16.         }  
  17.     })  
  18. @validator(rules)  
  19. class Person {  
  20.     name = 'tom'  
  21.     password = '123'  
  22.     age = '21'  
  23.  
  24. 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 不太一样) 

 

责任编辑:庞桂玉 来源: 前端大全
相关推荐

2022-09-21 09:04:07

Python装饰器

2024-02-23 12:11:53

装饰器模式对象

2020-12-29 09:56:29

浏览器缓存HTTP

2021-08-11 10:10:26

Linux定时器数组

2011-04-01 09:49:54

Python

2019-10-09 16:14:30

Web服务器Tomcat

2021-08-04 16:06:45

DataOps智领云

2023-12-22 19:59:15

2020-10-16 16:28:54

Python开发技术

2023-02-07 07:47:52

Python装饰器函数

2019-05-22 17:34:16

代码开发工具

2023-08-01 19:11:05

浏览器本地存储

2021-09-27 10:03:55

装饰器代码

2022-06-15 10:24:13

Pytho装饰器代码

2010-02-01 17:50:32

Python装饰器

2024-09-12 15:32:35

装饰器Python

2021-09-04 19:04:14

配置LogbackJava

2022-09-22 09:00:46

CSS单位

2018-09-28 14:06:25

前端缓存后端

2022-11-06 21:14:02

数据驱动架构数据
点赞
收藏

51CTO技术栈公众号