Decorators in ES7

ES6 已经来了,ES7 还会远么?

ES6 标准已于上个月(2015年6月17日)正式发布,众多新特性成为标准固然另人激动,然而更值得憧憬的还是未来。所以,让我们来看看 ES7(更正式的说法是 ES2016)有哪些激动人心的变化。最为人津津乐道的可能就是 async/await 了,不过我个人非常喜欢 Yehuda Katz 提出的 decorator 模式,本文尝试对此做一个介绍。

decorator 是什么

ES7 的 decorator 概念是从 Python 借来的,在 Python 里,decorator 实际上是一个 wrapper,它作用于一个目标函数,对这个目标函数做一些额外的操作,然后返回一个新的函数:

1
2
3
4
5
6
7
8
9
10
11
def my_decorator(fn):
def inner(name):
print 'Hello ' + fn(name)
return inner
@my_decorator
def greet(name):
return name
greet('Decorator!')
# Hello Decorator!

这种 @decorator 的写法其实是一种语法糖,从 my_decorator 的定义就可以看出,它接收一个函数(fn)为参数,定义一个新的内部函数(innner),这个内部函数会定义一些行为,最后 my_decorator 返回这个内部函数(inner)。

上面的 @my_decorator 等于:

1
2
3
4
greet = my_decorator(greet)
greet('Decorator!')
# Hello Decorator!

ES7 中的 decorator 同样借鉴了这个语法糖,不过依赖于 ES5 的 Object.defineProperty 方法 。

关于 Object.defineProperty 的一切(些)

defineProperty 所做的事情就是,为一个对象增加新的属性,或者更改对象某个已存在的属性。调用方式是 Object.defineProperty(obj, prop, descriptor),这 3 个参数分别代表:

  1. obj: 目标对象
  2. prop: 属性名
  3. descriptor: 针对该属性的描述符

有意思的是 descriptor 参数,它其实也是一个对象,其字段决定了 objprop 属性的一些特性。比如 enumerable 的真假就能决定目标对象是否可枚举(能够在 for…in 循环中遍历到,或者出现在 Object.keys 方法的返回值中),writable 决定目标对象的属性是否可以更改,等等。完整的描述符可选字段可以参看这里

作用在方法上的 decorator

先来看一个简单的类:

1
2
3
4
5
class Dog {
bark () {
return 'wang!wang!'
}
}

如果我们想让 bark 这个方法成为一个只读的属性,那么可以定义一个 readonly 的 decorator:

1
2
3
4
5
// 注意这里的 `target` 是 `Dog.prototype`
function readonly(target, key, descriptor) {
descriptor.writable = false
return descriptor
}

可以看到,decorator 就是一个普通函数,只不过它接收 3 个参数,与 Object.defineProperty 一致。具体在这里,我们就是把 descriptor 的 writable 字段设为 false

然后把 readonly 作用到 bark 方法上:

1
2
3
4
5
6
7
8
9
10
class Dog {
@readonly
bark () {
return 'wang!wang!'
}
}
let dog = new Dog()
dog.bark = 'bark!bark!'
// Cannot assign to read only property 'bark' of [object Object]

@readonly 具体做了什么呢?我们先来看一下 ES6 的 class 在转换为 ES5 代码之后是什么样的,即 Dog 这个 class 等价于:

1
2
3
4
5
6
7
8
9
10
// 步骤 1
function Dog () {}
// 步骤 2
Object.defineProperty(Dog.prototype, 'bark', {
value: function () { return 'wang!wang!' },
enumerable: false,
configurable: true,
writable: true
})

bark 方法应用 @readonly 之后,JS 引擎就会在进行步骤二之前调用这个 decorator:

1
2
3
4
5
6
7
8
9
10
let descriptor = {
value: function () { return 'wang!wang!' },
enumerable: false,
configurable: true,
writable: true
}
// decorator 接收的参数与 Object.defineProperty 一致
descriptor = readonly(Dog.prototype, 'bark', descriptor) || descriptor
Object.defineProperty(Dog.prototype, 'bark', descriptor)

所以,ES7 的 decorator,作用就是返回一个新的 descriptor,并把这个新返回的 descriptor 应用到目标方法上。稍后我们将会看到,decorator 并非只能作用到类的方法/属性上,它还可以作用到类本身。

作用在类上的 decorator

作用在方法上的 decorator 接收的第一个参数(target)是类的 prototype;如果把一个 decorator 作用到类上,则它的第一个参数 target 是类本身:

1
2
3
4
5
6
7
8
9
10
// 这里的 `target` 是类本身
function doge (target) {
target.isDoge = true
}
@doge
class Dog {}
console.log(Dog.isDoge)
// true

decorator 也可以是 factory function

如果我们想对不同的目标对象应用同一个 decorator,但同时又需要有一些差别,怎么办?很简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function doge (isDoge) {
return function(target) {
target.isDoge = isDoge
}
}
@doge(true)
class Dog {}
console.log(Dog.isDoge)
// true
@doge(false)
class Human {}
console.log(Human.isDoge)
// false

对方法来说也是类似的:

1
2
3
4
5
6
7
8
9
10
function enumerable (isEnumerable) {
return function(target, key, descriptor) {
descriptor.enumerable = isEnumerable
}
}
class Dog {
@enumerable(false)
eat () { }
}

decrator 能做什么

在 Python 里,decorator 的作用非常丰富,比如可以在使用 threading 时候简化分配锁和解锁的步骤,进行用户认证,定义一些快捷方式(如 @staticmethod@classmethod 之类),定义后端 api 的路由(如 Flask 框架)等等。

对 ES7 来说,现在已经有人写了一个 core-decorators,提供了一些非常实用的 decorator。

比如 @deprecate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { deprecate } from 'core-decorators'
class Dog {
@deprecate
peeInRoom () {}
@deprecate('I am a good dog.')
peeInBed () {}
}
let dog = new Dog()
dog.peeInRoom()
// DEPRECATION Dog#peeInRoom: This function will be removed in future versions.
dog.peeInBed()
// DEPRECATION Dog#peeInBed: I am a good dog.

除此之外,raganwaldJavaScript Allongé, the “Six” Edition 的作者)写了一篇文章介绍如何使用 decorator 来实现 mixin。这对框架和类库作者来说是一个重磅好消息。

现在就想用?

decorator 目前还只是一个提议,但是,感谢 Babel,我们现在就可以体验它了。首先,安装 babel:

1
npm install babel -g

然后,开启 decorator:

1
babel --optional es7.decorators foo.js > foo.es5.js

babel 也提供了一个在线的 REPL,勾选 experimental 选项,就可以了。

总结

decorator 让我们有机会在代码的执行期间改变其行为,我相信它在 Python 中可以做到的事情,在 ES7 中也同样能够做到。

参考

  1. Exploring ES2016 Decorators
  2. https://github.com/wycats/javascript-decorators
  3. https://github.com/jayphelps/core-decorators.js
  4. http://raganwald.com/2015/06/26/decorators-in-es7.html