深入 JavaScript 装饰器

更新时间:2020-03-24 11:11:12

人生的价值,并不是用时间,而是用深度去衡量的。 ——列夫·托尔斯泰

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

1. 前言

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

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

class Person {@readonly count = 0;
}

由于浏览器还未支持装饰器,为了让大家能够正常看到效果,这里我使用 Parcel 进行了一下简单的配置,可以去 clone 这个仓库后再来运行本文涉及到的所有例子,仓库地址:learn es6

本文涉及到 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。
装饰器与之前讲过的函数组合(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

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  Chinesestudent2.speak(); // I can speak  English

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

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 函数中,一般会接收三个参数:

  1. target:被修饰的类
  2. name:类成员的名字
  3. 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 {@timesay() {console.log('hello')}
}
const person = new Person();
person.say();

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

import {observable,action,autorun
} from 'mobx'class Store {@observable count = 1;@actionchangeCount(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@logsay() {}
}

除此之外,在装饰器的提案中,还出现了一种组合了多种装饰器的装饰器例子。目前还没见到被使用。
通过使用 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)}}}
}

如果对节流和防抖函数比较感兴趣,那么可以去阅读一下这篇文章:函数节流与函数防抖

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')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();

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. 推荐阅读:

  1. 装饰器 —— 阮一峰
  2. JS 装饰器(Decorator)场景实战
  3. 探索JavaScript中的装饰器模式
  4. 王者荣耀之「装饰者模式

深入 JavaScript 装饰器相关推荐

  1. Javascript 装饰器极速指南

    pablo.png Decorators 是ES7中添加的JavaScript新特性.熟悉Typescript的同学应该更早的接触到这个特性,TypeScript早些时候已经支持Decorators的 ...

  2. JavaScript装饰器模式

    装饰器模式 适配器模式是原来的接口就不能用了,需要做一个适配之后才能继续使用. 装饰器是原先接口还能用,在此基础上添加一些新功能.(为对象添加新功能,并且不改变原有结构和功能) 下面我们来举一个装饰器 ...

  3. JavaScript装饰器

    JS装饰器是ES7的新语法,浏览器,nodejs不一定支持,需要babel转译. 实验环境 创建一个目录test,结构为: . ├── .babelrc ├── .npmrc ├── lib ├── ...

  4. Vue中用TypeScript改写JavaScript及装饰器使用

    TypeScrpt相比JavaScript的主要特点 多了属性声明类型,格式:属性名:声明类型 name:string=""; //此时name属性声明类型为string 复制代码 ...

  5. php和python对比-PHP、Python和Javascript的装饰器模式对比

    修饰模式(Decorator Pattern),又叫装饰者模式,是面向对象编程领域中,一种动态地往一个类中添加新的行为的设计模式.就功能而言,修饰模式相比生成子类更为灵活,这样可以给某个对象而不是整个 ...

  6. JavaScript学习笔记(四十四) 装饰器

    装饰器模式(Decorator) 在装饰器模式中,可以在运行时给一个对象动态的添加额外的功能.当和静态类打交道的时候(static classes),这可能是一个挑战.但在JavaScript中,对象 ...

  7. javascript设计模式之装饰器模式(结构型模式)

    javascript设计模式之装饰器模式 js的设计模式分为创建型模式,结构型模式和行为模式 结构模式描述了如何组合对象以提供新的功能. 装饰器模式是一种常见的结构型模式,我们可以以一个基础对象为基础 ...

  8. Python:高级主题之(属性取值和赋值过程、属性描述符、装饰器)

    属性取值和赋值过程 一切皆是对象,类型也是对象. 对象包含一个__class__属性指向其所属类型. 对象包含一个__dict__属性指向其所包含的成员(属性和方法). 取值过程(下面是伪代码) 1 ...

  9. Django中六个常用的自定义装饰器

    装饰器作用 decorator是当今最流行的设计模式之一,很多使用它的人并不知道它是一种设计模式.这种模式有什么特别之处? 有兴趣可以看看Python Wiki上例子,使用它可以很方便地修改对象行为, ...

最新文章

  1. error C1189: #error : WINDOWS.H already included. MFC apps must not #include windows.h
  2. 【Python】嫦娥探月数据(PDS)处理与可视化
  3. Java中泛型Class《T》、T与Class《?》
  4. elasticsearch group by java_java elasticsearch聚合查询实例详解
  5. 图像处理中ct图的通道是多少_新一代安检CT机,智能安防领域又一明星产品
  6. 如何基于TensorFlow使用LSTM和CNN实现时序分类任务
  7. java用cookie最新浏览商品_jQuery.cookie.js实现记录最近浏览过的商品功能示例
  8. 【报告分享】2020程序员人群洞察报告.pdf(附下载链接)
  9. 嵌入不同源的页面_嵌入式技术课程教与学(教学大纲和试卷)
  10. js打印服务器文件,用Electron / Node.js编写的打印服务器
  11. 数据结构c语言版实验报告2,数据结构(C语言版) 实验报告 (2)
  12. 设计模式——工厂模式(包含:简单工厂模式、工厂方法模式、抽象工厂模式)
  13. 深度系统安装移动硬盘启动_Legacy无损更改UEFI启动并安装双系统
  14. UVA 1449 Dominating Patterns(AC自动机)
  15. GitLab系列3 Unicorn
  16. 大学c语言第三章作业,第三章_C语言标准课件_ppt_大学课件预览_高等教育资讯网...
  17. 机器学习和NLP面试总结
  18. android 关闭蓝牙功能,android – 打开和关闭蓝牙?
  19. 野人岛4四季神器java,十一天,四季的感受,顶风冒雪美国中西部自驾旅游之五-黄石:梦想之地(1)...
  20. 基于vs2019的openGL项目配置

热门文章

  1. 【原创】OllyDBG 入门系列(一)-认识OllyDBG
  2. Magento: 单产品(product)或者当前类别(category)最大和最小价格 Min/Max Product Price in a Category...
  3. Linux/Ubuntu: 使用 trash-cli 防止 rm 命令误删除重要文件
  4. 大数据量的存储分表常见算法
  5. PHP+Mysql+jQuery实现发布微博程序--PHP篇
  6. jQuery弹出层登录和全屏注册表单
  7. 安装WordPress图解
  8. C++的继承知识点重温
  9. 每日一题(C语言基础篇)3
  10. Linux——userdel删除用户不完全问题