1. 装饰器的概念
1-0. 前言
装饰器 (Decorator) 是 ES2017 中的一个提案,装饰器的出现,给我们在多个不同类之间共享或者扩展一些方法或者行为的时候,提供了一种更加优雅的方法。
1-1. 什么是装饰器
简单的说,修饰器就是一个对类进行处理的函数,可以给类以及类的方法添加一些行为,而又不改变其代码。
在 Python 中,对于 装饰器 (Decorator) 是这样定义的:
A Python decorator is a function that takes another function, extending the behavior of the latter function without explicitly modifying it.
Python 装饰器是接受另一个函数作为参数的函数,扩展参数函数行为的同时并没有修改参数函数本身。
我们通过一个简单的例子来说明装饰器是什么,以下代码是一个给类添加属性 isTestable
的装饰器,通过 @testable
这样的调用,可以给类增加一个 isTestable = true
的属性。
1 | @testable |
1-2. 设计模式: 装饰模式
装饰模式是 包装模式 (Wrapper Pattern) 的一种,同为包装模式的还有适配器模式。装饰模式以对客户端透明的方式扩展对象的功能,是继承关系的一个替代方案。
装饰模式的设计模式是为了动态地给一个对象增加额外的行为,单就增加功能来说,装饰模式要比生成子类的方式更加灵活。遇到符合下面描述的情况时,可以考虑使用装饰模式:
- 需要扩展一个类的功能,或者给一个类增加属性
- 需要动态的给一个对象增加功能,这些功能需要动态的撤销
- 需要增加一些基本功能的排列组合而产生的非常大量的功能,从而使继承变得不现实
2. ES6 中的装饰器
ES6 中装饰器使用特殊的语法,使用 @
作为标识符,且放置在被装饰代码之前。一个类可以被多个不同的装饰器装饰,在代码进行编译的时候,按照顺序相应执行。
1 | @log() |
此外,装饰器对于类的行为的改变,是在代码编译的时候发生的,并不是在运行时。这意味着,修饰器能在编译阶段运行代码。也就是说,修饰器本质就是编译时执行的函数。
2-1. 关于 Object.defineProperty
ES6 中的装饰器依赖于 ES5 中的 Object.defineProperty
,Object.defineProperty
方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。
Object.defineProperty
的语法如下:
Object.defineProperty(obj, prop, descriptor)
参数 obj
为要在其上定义属性的对象,prop
为要定义或者修改的属性的名称,descripor
为将被定义或修改的属性描述符,对于属性描述符有哪些,可以看这里。
该方法返回被传递给函数的对象。
Object.defineProperty
允许精确添加或修改对象的属性。通过赋值来添加的普通属性会创建在属性枚举期间显示的属性( for...in
或 Object.keys
方法), 这些值可以被改变,也可以被删除。这种方法允许这些额外的细节从默认值改变。默认情况下,使用 Object.defineProperty()
添加的属性值是不可变的。
2-2. 装饰器的使用
接下来我们通过几个简单的例子来看看,如何使用 ES6 提供的语法糖。
2-2-1. 对类的装饰器
像我们在 1-1 中举的例子 testable
就是一个对类进行装饰的例子,这个例子可以用带参数的方法实现,类似于工厂方法。
1 | function testable(isTestable) { |
前面的例子是为类添加一个静态属性,如果想添加实例属性,可以通过目标类的prototype对象操作。
1 | function testable(target) { |
2-2-2. 对类属性的装饰器
类属性装饰器适用于类的单独成员,接收三个参数:
target
被修饰的类name
要修饰的类成员的名字descriptor
要修饰的类成员的描述符
可以看到接收参数与 Object.defineProperty
完全类似。
对于类属性的装饰,可以先看一个非常常见的只读实例:
1 | function readonly(target, name, descriptor) { |
可以看到对 wangcai
的 say
方法进行修改并没有生效,此时 Cat
对象的 say
方法是只读的,不可被赋值表达式改变。
2-2-3. 装饰器用于函数
从上文中我们知道,实际上装饰器在代码编译时就运行了,存在函数提升,可以看下面的例子:
1 | var counter = 0; |
由于函数提升的存在,实际执行的代码如下:
1 | @add |
我们想让 counter
输出为 1 ,而counter
在最后给赋值了 0。
3. 应用实例
3-1. debounce (去抖动)
函数执行次数过于频繁导致性能问题的时候,debounce (去抖动) 可以节约性能提高用户体验。debounce (去抖动)的定义是:
如果用手指一直按住一个弹簧,它将不会弹起直到你松手为止。
去抖动通过限制函数执行次数,来提高用户体验。当调用动作 n 毫秒后,才会执行该动作,若在这 n 毫秒内又调用此动作则将重新计算执行时间。
1 | // core-decorators/src/debounce.js |
3-2. 混入 (Mixin)
混入 (Mixin) 所作的事情即枚举出一个或者多个对象的所有属性,然后将这些属性添加到另一个对象上去。
而实际上 jQuery 中的 jQuery.extend
和 lodash 中的 _.mixin
通过不同的调用形式都实现了混入 (Mixin)。
依赖于 Object.assign
我们可以用装饰器的方式实现实现混入 (Mixin):
1 | function mixins(...list) { |
3-3. 简单的日志系统
通过在给类执行前后增加输出的处理,可以实现监控类行为的功能:
1 | let log = (type) => { |
4. 通过 Babel 使用
使用 npm 安装 babel-plugin-transform-decorators-legacy 插件:
1 | npm install babel-plugin-transform-decorators-legacy --save-dev |
配置 .babelrc
文件:
1 | "plugins": ["transform-decorators-legacy"] |
参考资料: