我跟你说,我最讨厌“简介”这种文章了,要不是语文是体育老师教的,早就换标题了!
Decorators是ECMAScript现在处于Stage 1的一个提案。当然ECMAScript会有很多新的特性,特地介绍这一个是因为它能够在实际的编程中提供很大的帮助,甚至于改变不少功能的设计。
先说说怎么回事
如果光从概念上来介绍的话,官方是这么说的:
Decorators make it possible to annotate and modify classes and properties at design time.
我翻译一下:
装饰器让你可以在设计时对类和类的属性进行注解和修改。
什么鬼,说人话!
所以我们还是用一段代码来看一下好了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
function memoize(target, key, descriptor) { let cache = new Map(); let oldMethod = descriptor.value; descriptor.value = function (...args) { let hash = args[0]; if (cache.has(hash)) { return cache.get(hash); } let value = oldMethod.apply(this, args); cache.set(hash, value); return value; }; } class Foo { @memoize; getFooById(id) { // ... } } |
别去试上面的代码,瞎写的,估计跑不起来就是了。这个代码的作用其实看函数的命名就能明白,我们要给Foo#getFooById
方法加一个缓存,缓存使用第一个参数作为对应的键。
可以看出来,上面代码的重点在于:
- 有一个
memoize
函数。 - 在类的某个方法上加了
@memoize;
这样一个标记。
而这个@memoize
就是所谓的Decorator,我称之为装饰器。一个装饰器有以下特点:
- 首先它是一个函数。
- 这个函数会接收3个参数,分别是
target
、key
和descriptor
,具体的作用后面再说。 - 它可以修改
descriptor
做一些额外的逻辑。
看到了基本用法其实并不能说明什么,我们有几个核心的问题有待说明:
有几种装饰器
现阶段官方说有2种装饰器,但从实际使用上来看是有4种,分别是:
- 放在
class
上的“类装饰器”。 - 放在属性上的“属性装饰器”,这需要配合另一个Stage 0的类属性语法提案,或者只能放在对象字面量上了。
- 放在方法上的“方法装饰器”。
- 放在
getter
或setter
上的“访问器装饰器”。
其中类装饰器只能放在class
上,而另外3种可以同时放在class
和属性或者对象字面量的属性上,比如这样也是可以的:
1 2 3 4 5 6 |
let foo = { @memoize getFooById(id) { // ... } }; |
不过注意放在对象字面量时,装饰器后面不能写分号,这是个比较怪异的问题,后面还会说到更怪异的情况,我也在和提案的作者沟通这是为啥。
之所以这么分,是因为不同情况下,装饰器接收的3个参数代表的意义并不相同。
装饰器的3个参数是什么
装饰器接收3个参数,分别是target
、key
和descriptor
,他们各自分别是什么值,用一段代码就能很容易表达出来:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
function log(target, key, descriptor) { console.log(target); console.log(target.hasOwnProperty('constructor')); console.log(target.constructor); console.log(key); console.log(descriptor); } class Bar { @log; bar() {} } // {} // true // function Bar() { ... // bar // {"enumerable":false,"configurable":true,"writable":true} |
这是使用babel转换的JavaScript的输出,从这里可以看到:
key
很明显就是当前方法名,我们可以推断出来用于属性的时候就是属性名descriptor
显然是一个PropertyDescriptor
,就是我们用于defineProperty
时的那个东西。target
确实不是那么容易看出来,所以我用了3行代码。首先这是一个对象,然后是一个有constructor
属性的对象,最后constructur
指向的是Bar
这个函数。所以我们也能推测出来这货就是Bar.prototype
没跑了。
那如果装饰器放在对象字面量上,而不是类上呢?这边就不再给代码,直接放结论了:
key
和descriptor
和放在类属性/方法上一样没变,这当然也不应该变。target
是Object
对象,相信我你不会想用这个参数的。
当装饰器放在属性、方法、访问器上时,都符合上面的原则,但放在类上的时候,有一些不同:
key
和descriptor
不会提供,只有target
参数。target
会变成Bar
这个方法,而不是其prototype
。
其实对于属性、方法和访问器,真正有用的就是descriptor
,其它几个无视问题也不大就是了。而对于类,由于target
是唯一能用的,所以会需要它。
对于这一环节,我们需要特别注意一点,由于target
是类的prototype
,所以往它上面添加属性是,要注意继承时是会被继承下去的,而子类上再加同样属性又会有覆盖甚至对象、数组同引用混在一起的问题。这和我们平时尽量不在prototype
上放对象或者数组的思路是一致的,要避免这一问题。
装饰器在什么时候执行
既然装饰器本身是一个函数,那么自然要有函数被执行的时候。
现阶段,装饰器只能放在一个类或者一个对象上,我们可以用代码看一下什么时候执行:
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 |
// 既然装饰器是函数,我当然可以用函数工厂了 function log(message) { return function() { console.log">return function() { console.log个提案。当然ECMAScript会有很多新的特性,特地介绍这一个是因为它能够在实际的编程中提供很大的帮助,甚至于改变不少功能的设计。
先说说怎么回事如果光从概念上来介绍的话,官方是这么说的:
我翻译一下:
什么鬼,说人话! 所以我们还是用一段代码来看一下好了:
别去试上面的代码,瞎写的,估计跑不起来就是了。这个代码的作用其实看函数的命名就能明白,我们要给 可以看出来,上面代码的重点在于:
而这个
看到了基本用法其实并不能说明什么,我们有几个核心的问题有待说明: 有几种装饰器现阶段官方说有2种装饰器,但从实际使用上来看是有4种,分别是:
其中类装饰器只能放在
不过注意放在对象字面量时,装饰器后面不能写分号,这是个比较怪异的问题,后面还会说到更怪异的情况,我也在和提案的作者沟通这是为啥。 之所以这么分,是因为不同情况下,装饰器接收的3个参数代表的意义并不相同。 装饰器的3个参数是什么装饰器接收3个参数,分别是
这是使用babel转换的JavaScript的输出,从这里可以看到:
那如果装饰器放在对象字面量上,而不是类上呢?这边就不再给代码,直接放结论了:
当装饰器放在属性、方法、访问器上时,都符合上面的原则,但放在类上的时候,有一些不同:
其实对于属性、方法和访问器,真正有用的就是 对于这一环节,我们需要特别注意一点,由于 装饰器在什么时候执行既然装饰器本身是一个函数,那么自然要有函数被执行的时候。 现阶段,装饰器只能放在一个类或者一个对象上,我们可以用代码看一下什么时候执行:
|