装饰器及其相关实例

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
2
3
4
5
6
7
8
9
10
@testable
class MyTestableClass {
// ...
}

function testable(target) {
target.isTestable = true;
}

MyTestableClass.isTestable // true

1-2. 设计模式: 装饰模式

装饰模式是 包装模式 (Wrapper Pattern) 的一种,同为包装模式的还有适配器模式。装饰模式以对客户端透明的方式扩展对象的功能,是继承关系的一个替代方案。

装饰模式的设计模式是为了动态地给一个对象增加额外的行为,单就增加功能来说,装饰模式要比生成子类的方式更加灵活。遇到符合下面描述的情况时,可以考虑使用装饰模式:

  • 需要扩展一个类的功能,或者给一个类增加属性
  • 需要动态的给一个对象增加功能,这些功能需要动态的撤销
  • 需要增加一些基本功能的排列组合而产生的非常大量的功能,从而使继承变得不现实

2. ES6 中的装饰器

ES6 中装饰器使用特殊的语法,使用 @ 作为标识符,且放置在被装饰代码之前。一个类可以被多个不同的装饰器装饰,在代码进行编译的时候,按照顺序相应执行。

1
2
3
4
5
6
7
8
@log()
@immutable()
class Example {
@time('demo')
doSomething() {

}
}

此外,装饰器对于类的行为的改变,是在代码编译的时候发生的,并不是在运行时。这意味着,修饰器能在编译阶段运行代码。也就是说,修饰器本质就是编译时执行的函数。

2-1. 关于 Object.defineProperty

ES6 中的装饰器依赖于 ES5 中的 Object.definePropertyObject.defineProperty 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。

Object.defineProperty 的语法如下:

Object.defineProperty(obj, prop, descriptor)

参数 obj 为要在其上定义属性的对象,prop 为要定义或者修改的属性的名称,descripor 为将被定义或修改的属性描述符,对于属性描述符有哪些,可以看这里
该方法返回被传递给函数的对象。

Object.defineProperty允许精确添加或修改对象的属性。通过赋值来添加的普通属性会创建在属性枚举期间显示的属性( for...inObject.keys 方法), 这些值可以被改变,也可以被删除。这种方法允许这些额外的细节从默认值改变。默认情况下,使用 Object.defineProperty() 添加的属性值是不可变的。

2-2. 装饰器的使用

接下来我们通过几个简单的例子来看看,如何使用 ES6 提供的语法糖。

2-2-1. 对类的装饰器

像我们在 1-1 中举的例子 testable 就是一个对类进行装饰的例子,这个例子可以用带参数的方法实现,类似于工厂方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
function testable(isTestable) {
return function(target) {
target.isTestable = isTestable;
}
}

@testable(true)
class MyTestableClass {}
MyTestableClass.isTestable // true

@testable(false)
class MyClass {}
MyClass.isTestable // false

前面的例子是为类添加一个静态属性,如果想添加实例属性,可以通过目标类的prototype对象操作。

1
2
3
4
5
6
7
8
9
function testable(target) {
target.prototype.isTestable = true;
}

@testable
class MyTestableClass {}

let obj = new MyTestableClass();
obj.isTestable // true

2-2-2. 对类属性的装饰器

类属性装饰器适用于类的单独成员,接收三个参数:

  • target 被修饰的类
  • name 要修饰的类成员的名字
  • descriptor 要修饰的类成员的描述符

可以看到接收参数与 Object.defineProperty 完全类似。

对于类属性的装饰,可以先看一个非常常见的只读实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function readonly(target, name, descriptor) {
discriptor.writable = false;
return discriptor;
}
class Cat {
@readonly
say() {
console.log("meow");
}
}
var wangcai = new Cat();
wangcai.say = function() {
console.log("woof");
}

// Exception: Attempted to assign to readonly property

可以看到对 wangcaisay 方法进行修改并没有生效,此时 Cat 对象的 say 方法是只读的,不可被赋值表达式改变。

2-2-3. 装饰器用于函数

从上文中我们知道,实际上装饰器在代码编译时就运行了,存在函数提升,可以看下面的例子:

1
2
3
4
5
6
7
8
9
var counter = 0;

var add = function () {
counter++;
};

@add
function foo() {
}

由于函数提升的存在,实际执行的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
@add
function foo() {
}

var counter;
var add;

counter = 0;

add = function () {
counter++;
};

我们想让 counter 输出为 1 ,而counter 在最后给赋值了 0。

3. 应用实例

3-1. debounce (去抖动)

函数执行次数过于频繁导致性能问题的时候,debounce (去抖动) 可以节约性能提高用户体验。debounce (去抖动)的定义是:

如果用手指一直按住一个弹簧,它将不会弹起直到你松手为止。

去抖动通过限制函数执行次数,来提高用户体验。当调用动作 n 毫秒后,才会执行该动作,若在这 n 毫秒内又调用此动作则将重新计算执行时间。

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
28
29
30
31
32
33
34
35
36
37
38
39
40
// core-decorators/src/debounce.js
import { decorate, metaFor, internalDeprecation } from './private/utils';

const DEFAULT_TIMEOUT = 300;

function handleDescriptor(target, key, descriptor, [wait = DEFAULT_TIMEOUT, immediate = false]) {
const callback = descriptor.value;

if (typeof callback !== 'function') {
throw new SyntaxError('Only functions can be debounced');
}

return {
...descriptor,
value() {
const { debounceTimeoutIds } = metaFor(this); // 每个 debounce 使用独立的计时器
const timeout = debounceTimeoutIds[key];
const callNow = immediate && !timeout;
const args = arguments;

clearTimeout(timeout);

debounceTimeoutIds[key] = setTimeout(() => {
delete debounceTimeoutIds[key];
if (!immediate) {
callback.apply(this, args);
}
}, wait);

if (callNow) {
callback.apply(this, args);
}
}
};
}

export default function debounce(...args) {
internalDeprecation('@debounce is deprecated and will be removed shortly. Use @debounce from lodash-decorators.\n\n https://www.npmjs.com/package/lodash-decorators');
return decorate(handleDescriptor, args);
}

3-2. 混入 (Mixin)

混入 (Mixin) 所作的事情即枚举出一个或者多个对象的所有属性,然后将这些属性添加到另一个对象上去。

而实际上 jQuery 中的 jQuery.extend和 lodash 中的 _.mixin 通过不同的调用形式都实现了混入 (Mixin)。

依赖于 Object.assign 我们可以用装饰器的方式实现实现混入 (Mixin):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function mixins(...list) {
return function (target) {
Object.assign(target.prototype, ...list);
};
}

const Foo = {
foo() { console.log('foo') }
};

@mixins(Foo)
class MyClass {}

let obj = new MyClass();
obj.foo() // "foo"

3-3. 简单的日志系统

通过在给类执行前后增加输出的处理,可以实现监控类行为的功能:

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
let log = (type) => {
return (target, name, descriptor) => {
const method = descriptor.value;
descriptor.value = (...args) => {
console.info(`(${type}) 正在执行: ${name}(${args}) = ?`);
let ret;
try {
ret = method.apply(target, args);
console.info(`(${type}) 成功 : ${name}(${args}) => ${ret}`);
} catch (error) {
console.error(`(${type}) 失败: ${name}(${args}) => ${error}`);
}
return ret;
}
}
}
class IronMan {
@log('IronMan 自检阶段')
check(){
return '检查完毕';
}
@log('IronMan 攻击阶段')
attack(){
return '击倒敌人';
}
@log('IronMan 机体报错')
error(){
throw 'Something is wrong!';
}
}

var tony = new IronMan();
tony.check();
tony.attack();
tony.error();

// (IronMan 自检阶段) 正在执行: check() = ?
// (IronMan 自检阶段) 成功 : check() => 检查完毕
// (IronMan 攻击阶段) 正在执行: attack() = ?
// (IronMan 攻击阶段) 成功 : attack() => 击倒敌人
// (IronMan 机体报错) 正在执行: error() = ?
// (IronMan 机体报错) 失败: error() => Something is wrong!

4. 通过 Babel 使用

使用 npm 安装 babel-plugin-transform-decorators-legacy 插件:

1
npm install babel-plugin-transform-decorators-legacy --save-dev

配置 .babelrc 文件:

1
"plugins": ["transform-decorators-legacy"]

参考资料: