class="markdown_views prism-atom-one-dark">
ES7 提案: Decorators 装饰器
前言
今天我们来说说一个 ES7 提出的实验性特性,截止目前为止还处于 stage-2 的 Decorators 装饰器。他的使用形式就好像 Java 里面的注解(Annotation)一样,然而其实现机制和能力却又比 Java 的标记型注解要强大许多,下面我们就来看看 ES7 装饰器的具体用法和效果。
正文
1. Decorator 装饰器使用规范
首先我们先看看装饰器的使用形态和使用范围,装饰器的表达式如下
class="prism language-js">@class="token operator"><decoratorclass="token operator">-expressionclass="token operator">>
使用 @
符号加上一个返回一个函数的表达式
装饰器通常是用于 “修饰” 某个目标,也就是为某个目标添加一些特性,同时装饰器能够装饰的目标也被局限为下列两种
- 类(ES6 的 class)
- 类属性
- 类实例属性(field)
- 类方法(method)
- 类访问器属性(accessor = getter/setter)
下面在进入实际的代码测试环节之前,我们先看看几个表现特性和前提
1.1 装饰器的使用形式 & 具体行为
前面提过装饰器实际上可以说是仅仅作为 ES6 的 class 的扩展属性,因为他不能用于装饰 ES5 以前的函数类,也不能用于装饰一般变量
所以说实际上装饰器的使用形式大致就是以下几种
class="prism language-js">@classDecorator
class="token keyword">class class="token class-name">MyClass class="token punctuation">{
@fieldDecorator
myField class="token operator">= class="token number">0
@methodDecorator
class="token function">fclass="token punctuation">(class="token punctuation">) class="token punctuation">{class="token punctuation">}
@accessorDecorator
class="token keyword">get class="token function">otherFieldclass="token punctuation">(class="token punctuation">) class="token punctuation">{class="token punctuation">}
class="token punctuation">}
而我们用来装饰目标的装饰器其本质上就是一个函数,同时根据装饰目标对象的不同接受不同的参数如下:
class="prism language-js">class="token comment">// 类装饰器
class="token keyword">function class="token function">classDecoratorclass="token punctuation">(targetclass="token punctuation">) class="token punctuation">{class="token comment">/* ... */class="token punctuation">}
class="token comment">// 类属性装饰器
class="token keyword">function class="token function">methodDecoratorclass="token punctuation">(targetclass="token punctuation">, nameclass="token punctuation">, descriptionclass="token punctuation">) class="token punctuation">{class="token comment">/* ... */class="token punctuation">}
后面我们会再详细说明不同装饰器接受的参数和返回值对装饰目标的影响
1.2 为何不能修饰函数?
细心的人可能会注意到,装饰器这么好用,但是却不能修饰一般对象,这是为什么呢?
我们看看如下代码段,假设装饰器可以装饰普通函数的话会发生什么事:
class="prism language-js">class="token keyword">var counter class="token operator">= class="token number">0class="token punctuation">;
class="token keyword">var class="token function-variable function">add class="token operator">= class="token keyword">function class="token punctuation">(class="token punctuation">) class="token punctuation">{
counterclass="token operator">++class="token punctuation">;
class="token punctuation">}class="token punctuation">;
@add
class="token keyword">function class="token function">fooclass="token punctuation">(class="token punctuation">) class="token punctuation">{class="token punctuation">}
按代码顺序我们可能预期的是:
1. 定义 counter 变量
2. 定义 add 装饰器
3. 定义 foo 函数并用 add 装饰,counter 记录方法数 +1
然而普通的函数其实存在所谓的 函数提升,也就是说实际作用的代码段应该如下
class="prism language-js">class="token keyword">var counterclass="token punctuation">;
class="token keyword">var addclass="token punctuation">;
@add
class="token keyword">function class="token function">fooclass="token punctuation">(class="token punctuation">) class="token punctuation">{class="token punctuation">}
counter class="token operator">= class="token number">0class="token punctuation">;
class="token function-variable function">add class="token operator">= class="token keyword">functionclass="token punctuation">(class="token punctuation">) class="token punctuation">{
counterclass="token operator">++
class="token punctuation">}
这时候实际上 foo
方法真正定义并存在的时候 add
装饰器还是空的,甚至 counter
变量都不一定被初始化好,所以实际上 counter
的结果为 0。
2. 详细说明 & 代码示例
好了基础认识都差不多了,下面来看看装饰器在不同目标上的具体行为与特性
2.1 类装饰器 Class Decorators
首先第一种我们先来看看目标为 ES6 的 class 时的 类装饰器(Class Decorator) 实现。
2.1.1 传入参数 & 基础用法
首先我们可以先用一个 test
装饰器来测试一下接受哪些参数了
class="prism language-js">class="token keyword">function class="token function">testclass="token punctuation">(class="token operator">...argsclass="token punctuation">) class="token punctuation">{
class="token keyword">let i class="token operator">= class="token number">0
argsclass="token punctuation">.class="token function">forEachclass="token punctuation">(class="token punctuation">(argclass="token punctuation">) class="token operator">=> class="token function">logclass="token punctuation">(iclass="token operator">++class="token punctuation">, argclass="token punctuation">)class="token punctuation">)
class="token punctuation">}
@test
class="token keyword">class class="token class-name">MyClass class="token punctuation">{class="token punctuation">}
0 [Function: MyClass]
我们看到作为类装饰器的时候只接受一个参数,也就是装饰的类定义本身,也就是说我们可以在这个阶段对类直接添加一些属性如下
class="prism language-js">class="token keyword">function class="token function">testableclass="token punctuation">(targetclass="token punctuation">) class="token punctuation">{
targetclass="token punctuation">._isTestable class="token operator">= class="token boolean">true
class="token punctuation">}
@testable
class="token keyword">class class="token class-name">TestableClass class="token punctuation">{class="token punctuation">}
class="token keyword">class class="token class-name">OtherClass class="token punctuation">{class="token punctuation">}
class="token function">logclass="token punctuation">(class="token string">'TestableClass: 'class="token punctuation">, TestableClassclass="token punctuation">)
class="token function">logclass="token punctuation">(class="token string">'TestableClass._isTestable: 'class="token punctuation">, TestableClassclass="token punctuation">._isTestableclass="token punctuation">)
class="token function">logclass="token punctuation">(class="token string">'OtherClass: 'class="token punctuation">, OtherClassclass="token punctuation">)
class="token function">logclass="token punctuation">(class="token string">'OtherClass._isTestable: 'class="token punctuation">, OtherClassclass="token punctuation">._isTestableclass="token punctuation">)
TestableClass: [Function: TestableClass] { _isTestable: true }
TestableClass._isTestable: true
OtherClass: [Function: OtherClass]
OtherClass._isTestable: undefined
我们定义了 testable
的装饰器,用于对对象添加 _isTestable
标记,这个用法就跟 Java 中的注解比较相似,仅仅是对目标类型打上一些标记。
还记得 ES6 的 class 仅仅是作为 ES5 以前的函数类的语法糖,也就是说 TestableClass、OtherClass
其实本质上就是作为类的构造方法如下
class="prism language-js">class="token keyword">function class="token function">TestableClassclass="token punctuation">(class="token punctuation">) class="token punctuation">{class="token punctuation">}
class="token keyword">function class="token function">OtherClassclass="token punctuation">(class="token punctuation">) class="token punctuation">{class="token punctuation">}
而这时候我们添加的 TestableClass_isTestable
、OtherClass._isTestable
是直接添加到构造方法的属性,对于类型来说就是只能透过类名访问的静态属性
2.1.2 装饰器返回值
既然我们已经知道装饰器实际上就是定义一个函数来对类进行修饰,那我就有点好奇,作为装饰器的函数的返回值又会有什么影响呢?
class="prism language-js">class="token keyword">import class="token punctuation">{ log class="token punctuation">} class="token keyword">from class="token string">'../utils'
class="token keyword">function class="token function">replaceWithPrimitiveclass="token punctuation">(targetclass="token punctuation">) class="token punctuation">{
class="token keyword">return class="token number">123
class="token punctuation">}
class="token keyword">function class="token function">replaceWithObjectclass="token punctuation">(targetclass="token punctuation">) class="token punctuation">{
class="token keyword">return class="token punctuation">{ target class="token punctuation">}
class="token punctuation">}
@replaceWithPrimitive
class="token keyword">class class="token class-name">A class="token punctuation">{class="token punctuation">}
@replaceWithObject
class="token keyword">class class="token class-name">B class="token punctuation">{class="token punctuation">}
class="token function">logclass="token punctuation">(class="token string">'class A: 'class="token punctuation">, class="token constant">Aclass="token punctuation">)
class="token function">logclass="token punctuation">(class="token string">'class B: 'class="token punctuation">, class="token constant">Bclass="token punctuation">)
class A: 123
class B: { target: [Function: B] }
这时候我们定义两个装饰器,一个是 replaceWithPrimitive
、一个是 replaceWithObject
,分别返回基础类型和一个新的对象,我们发现输出也不管,返回的是啥就是啥,加上前一小节无返回值的装饰器,我们就可以说实际上一个被修饰的类对象其实上与下列表达式等价
class="prism language-js">@func
class="token keyword">class class="token class-name">A class="token punctuation">{class="token punctuation">}
class="token comment">/* 等价于 */
class="token keyword">const class="token constant">A class="token operator">= class="token function">funcclass="token punctuation">(class="token constant">Aclass="token punctuation">) class="token operator">|| class="token constant">A
这个特性实际上就为装饰器的应用敞开了大门,甚至可以以装饰器作为函数构造代理的应用,不过本篇就不再做探讨,知道就行。
2.1.3 装饰器表达式
前面我们提过,@
符号后面接的是一个返回函数的表达式,也就是说我们不一定要直接使用装饰器函数的名字,只要传入一个 能返回装饰器函数的表达式 即可:
class="prism language-js">class="token keyword">import class="token punctuation">{ log class="token punctuation">} class="token keyword">from class="token string">'../utils'
class="token keyword">function class="token function">bindColorclass="token punctuation">(colorclass="token punctuation">) class="token punctuation">{
class="token keyword">return class="token punctuation">(targetclass="token punctuation">) class="token operator">=> class="token punctuation">{
targetclass="token punctuation">._color class="token operator">= color
class="token punctuation">}
class="token punctuation">}
@class="token function">bindColorclass="token punctuation">(class="token string">'red'class="token punctuation">)
class="token keyword">class class="token class-name">Red class="token punctuation">{class="token punctuation">}
@class="token function">bindColorclass="token punctuation">(class="token string">'green'class="token punctuation">)
class="token keyword">class class="token class-name">Green class="token punctuation">{class="token punctuation">}
@class="token function">bindColorclass="token punctuation">(class="token string">'Blue'class="token punctuation">)
class="token keyword">class class="token class-name">Blue class="token punctuation">{class="token punctuation">}
class="token function">logclass="token punctuation">(class="token string">'Red: 'class="token punctuation">, Redclass="token punctuation">)
class="token function">logclass="token punctuation">(class="token string">'Green: 'class="token punctuation">, Greenclass="token punctuation">)
class="token function">logclass="token punctuation">(class="token string">'Blue: 'class="token punctuation">, Blueclass="token punctuation">)
我们定义一个 bindColor
方法,传入 color
后返回将 color
绑定到类定义上的装饰器函数,看看效果
Red: [Function: Red] { _color: 'red' }
Green: [Function: Green] { _color: 'green' }
Blue: [Function: Blue] { _color: 'Blue' }
我们可以看到每个类都绑定了自己的属性,透过 bindColor
方法使得同一个装饰器函数能够被多次复用,也使装饰器函数的使用更加灵活
2.1.4 装饰器注入原型方法
前面提过对于 类装饰器(Class Decorator) 函数接受的参数只有一个就是类定义本身,前面的示例我们为类定义添加静态属性,下面我再告诉你他还能访问类定义的 prototype
属性进而添加甚至修改原型方法
class="prism language-js">class="token keyword">import class="token punctuation">{ log class="token punctuation">} class="token keyword">from class="token string">'../utils'
class="token keyword">function class="token function">infoclass="token punctuation">(targetclass="token punctuation">) class="token punctuation">{
targetclass="token punctuation">.prototypeclass="token punctuation">.class="token function-variable function">greeting class="token operator">= class="token keyword">function class="token punctuation">(class="token punctuation">) class="token punctuation">{
consoleclass="token punctuation">.class="token function">logclass="token punctuation">(class="token template-string">class="token string">`This is class class="token interpolation">class="token interpolation-punctuation punctuation">${class="token keyword">thisclass="token punctuation">.nameclass="token interpolation-punctuation punctuation">}class="token string">`class="token punctuation">)
class="token punctuation">}
class="token punctuation">}
@info
class="token keyword">class class="token class-name">A class="token punctuation">{class="token punctuation">}
class="token keyword">const a class="token operator">= class="token keyword">new class="token class-name">Aclass="token punctuation">(class="token punctuation">)
aclass="token punctuation">.name class="token operator">= class="token string">'a instance of class A'
consoleclass="token punctuation">.class="token function">logclass="token punctuation">(class="token string">'A: 'class="token punctuation">, class="token constant">Aclass="token punctuation">)
consoleclass="token punctuation">.class="token function">logclass="token punctuation">(class="token string">'a: 'class="token punctuation">, aclass="token punctuation">)
aclass="token punctuation">.class="token function">greetingclass="token punctuation">(class="token punctuation">)
这里我们透过 target.prototype.greeting = function () {/* ... */}
来对 class A
添加一个原型方法 greeting
,这时候我们就可以在类实例上调用 a.greeting
原型方法,输出:
A: [Function: A]
a: A { name: 'a instance of class A' }
This is class a instance of class A
也就是说我们透过拿到的 target
类定义可以对类型进行非常灵活的方法扩展和静态标记,而这有没有让你想起前端领域非常常见的 混入(Mixin)设计模式
,下面来看看。
2.1.5 装饰器实现混入(Mixin)模式
装饰器实际上就是一个非常适合实现混入设计模式的地点。在混入设计模式中,我们通常是对于某个现存的类型进行原型方法的混入,在 Vue 源码实习中也被大量使用:
class="prism language-js">class="token keyword">function class="token function">Vueclass="token punctuation">(class="token punctuation">) class="token punctuation">{class="token punctuation">}
Vueclass="token punctuation">.prototypeclass="token punctuation">.class="token function-variable function">_init class="token operator">= class="token keyword">functionclass="token punctuation">(class="token punctuation">) class="token punctuation">{class="token comment">/* ... */class="token punctuation">}
Vueclass="token punctuation">.prototypeclass="token punctuation">.class="token function-variable function">$set class="token operator">= class="token keyword">functionclass="token punctuation">(class="token punctuation">) class="token punctuation">{class="token comment">/* ... */class="token punctuation">}
Vueclass="token punctuation">.prototypeclass="token punctuation">.class="token function-variable function">$delete class="token operator">= class="token keyword">functionclass="token punctuation">(class="token punctuation">) class="token punctuation">{class="token comment">/* ... */class="token punctuation">}
class="token comment">// ...
那么这个新的装饰器是不是根本就像是为了混入设计模式量身定做的特性呢:
class="prism language-js">@class="token function">mixinclass="token punctuation">(class="token punctuation">{ _initclass="token punctuation">: initclass="token punctuation">, $class="token keyword">setclass="token punctuation">: class="token keyword">setclass="token punctuation">, $class="token keyword">deleteclass="token punctuation">, del class="token punctuation">}class="token punctuation">)
class="token keyword">class class="token class-name">Vue class="token punctuation">{class="token comment">/* ... */class="token punctuation">}
下面我们看看自定义的代码示例:
class="prism language-js">class="token keyword">import class="token punctuation">{ log class="token punctuation">} class="token keyword">from class="token string">'../utils'
class="token keyword">function class="token function">mixinclass="token punctuation">(class="token operator">...methodsclass="token punctuation">) class="token punctuation">{
class="token keyword">return class="token punctuation">(targetclass="token punctuation">) class="token operator">=> class="token punctuation">{
Objectclass="token punctuation">.class="token function">assignclass="token punctuation">(targetclass="token punctuation">.prototypeclass="token punctuation">, class="token operator">...methodsclass="token punctuation">)
class="token punctuation">}
class="token punctuation">}
class="token keyword">const humanActions class="token operator">= class="token punctuation">{
class="token function">greetingclass="token punctuation">(class="token punctuation">) class="token punctuation">{
class="token function">logclass="token punctuation">(class="token template-string">class="token string">`this is class="token interpolation">class="token interpolation-punctuation punctuation">${class="token keyword">thisclass="token punctuation">.nameclass="token interpolation-punctuation punctuation">}class="token string">`class="token punctuation">)
class="token punctuation">}class="token punctuation">,
class="token punctuation">}
class="token keyword">const birdActions class="token operator">= class="token punctuation">{
class="token function">flyclass="token punctuation">(class="token punctuation">) class="token punctuation">{
class="token function">logclass="token punctuation">(class="token string">'I can fly'class="token punctuation">)
class="token punctuation">}class="token punctuation">,
class="token punctuation">}
@class="token function">mixinclass="token punctuation">(humanActionsclass="token punctuation">, birdActionsclass="token punctuation">)
class="token keyword">class class="token class-name">A class="token punctuation">{class="token punctuation">}
class="token function">logclass="token punctuation">(class="token string">'A: 'class="token punctuation">, class="token constant">Aclass="token punctuation">)
class="token function">logclass="token punctuation">(class="token string">'A.prototype: 'class="token punctuation">, class="token constant">Aclass="token punctuation">.prototypeclass="token punctuation">)
这里我们定义一个 mixin
方法,接受多个方法集合对象做参数,并使用 Object.assign
方法注入 target.prototype
原型对象中,借此就可以向现存类型注入新的可用方法
A: [Function: A]
A.prototype: A { greeting: [Function: greeting], fly: [Function: fly] }
2.2 类方法装饰器 Class Method Decorators
第二种应用场景是类属性装饰器,我们以 类方法装饰器(Class Method Decorators) 为代表
2.2.1 传入参数 & 基础用法
我们知道其实在 class
关键字后定义的方法、属性、访问器属性其实都是作为某个对象的属性(原型方法、实例属性、实例访问器属性),也就是说其实他们接受的参数类型是非常相似的:
class="prism language-js">class="token keyword">import class="token punctuation">{ log class="token punctuation">} class="token keyword">from class="token string">'../utils'
class="token keyword">function class="token function">testclass="token punctuation">(class="token operator">...argsclass="token punctuation">) class="token punctuation">{
argsclass="token punctuation">.class="token function">forEachclass="token punctuation">(class="token punctuation">(argclass="token punctuation">, iclass="token punctuation">) class="token operator">=> class="token function">logclass="token punctuation">(iclass="token punctuation">, argclass="token punctuation">)class="token punctuation">)
class="token punctuation">}
class="token keyword">class class="token class-name">A class="token punctuation">{
@test
field class="token operator">= class="token number">0
@test
class="token function">fclass="token punctuation">(class="token punctuation">) class="token punctuation">{class="token punctuation">}
@test
class="token keyword">get class="token function">nameclass="token punctuation">(class="token punctuation">) class="token punctuation">{class="token punctuation">}
class="token punctuation">}
0 A {}
1 field
2 {
configurable: true,
enumerable: true,
writable: true,
initializer: [Function: initializer]
}
0 A {}
1 f
2 {
value: [Function: f],
writable: true,
enumerable: false,
configurable: true
}
0 A {}
1 name
2 {
get: [Function: get],
set: undefined,
enumerable: false,
configurable: true
}
我们可以看到针对三种目标都接受三个参数:
- 类定义对象
- 方法/实例属性/实例访问器属性名
- 属性描述符(description)
这里的属性描述符其实就跟 Object.defineProperty
的第三个参数相似,就是一些描述对象属性的标志(configurable
可配置性、enumerable
可遍历性、writable
可写性),而不同对象有不同的默认值可以看到上面的输出
也就是说精确的函数标签如下
class="prism language-js">class="token keyword">class class="token class-name">B class="token punctuation">{
@test2
class="token function">fclass="token punctuation">(class="token punctuation">) class="token punctuation">{class="token punctuation">}
class="token punctuation">}
class="token keyword">function class="token function">test2class="token punctuation">(targetclass="token punctuation">, nameclass="token punctuation">, descclass="token punctuation">) class="token punctuation">{
class="token function">logclass="token punctuation">(class="token string">'target: 'class="token punctuation">, targetclass="token punctuation">)
class="token function">logclass="token punctuation">(class="token string">'name: 'class="token punctuation">, nameclass="token punctuation">)
class="token function">logclass="token punctuation">(class="token string">'desc: 'class="token punctuation">, descclass="token punctuation">)
class="token punctuation">}
target: B {}
name: f
desc: {
value: [Function: f],
writable: true,
enumerable: false,
configurable: true
}
这时候其实留下了一个扩展点:我们可不可以透过定义属性的 getter/setter 来对属性进行访问的扩展,而这也就为后续的装饰器应用留下极大的扩展空间(当然更完整的方法扩展还是推荐使用 Proxy 代理对象)
2.2.2 readonly 只读属性
第一种应用我们可以定义用于类方法的只读装饰器实现
class="prism language-js">class="token keyword">import class="token punctuation">{ log class="token punctuation">} class="token keyword">from class="token string">'../utils'
class="token keyword">function class="token function">readonlyclass="token punctuation">(targetclass="token punctuation">, nameclass="token punctuation">, descclass="token punctuation">) class="token punctuation">{
descclass="token punctuation">.writable class="token operator">= class="token boolean">false
descclass="token punctuation">.enumerable class="token operator">= class="token boolean">true
class="token punctuation">}
class="token keyword">class class="token class-name">A class="token punctuation">{
@readonly
class="token function">fclass="token punctuation">(class="token punctuation">) class="token punctuation">{class="token punctuation">}
class="token punctuation">}
class="token function">logclass="token punctuation">(class="token string">'A.prototype'class="token punctuation">, class="token constant">Aclass="token punctuation">.prototypeclass="token punctuation">)
class="token keyword">try class="token punctuation">{
class="token constant">Aclass="token punctuation">.prototypeclass="token punctuation">.f class="token operator">= class="token string">'new one'
class="token punctuation">} class="token keyword">catch class="token punctuation">(class="token class-name">eclass="token punctuation">) class="token punctuation">{
class="token function">logclass="token punctuation">(eclass="token punctuation">)
class="token punctuation">}
A.prototype A { f: [Function: f] }
TypeError: Cannot assign to read only property 'f' of object '#<A>'
我们可以看到当我们尝试重新对 @readonly f() {}
重新赋值的时候就会因为 writable: false
而报错
2.2.3 logger 日志装饰器
第二种是定义一个 logger
作为日志装饰器,记录一个对象指定方法的各个调用记录
class="prism language-js">class="token keyword">import class="token punctuation">{ log class="token punctuation">} class="token keyword">from class="token string">'../utils'
class="token keyword">function class="token function">loggerclass="token punctuation">(targetclass="token punctuation">, nameclass="token punctuation">, descclass="token punctuation">) class="token punctuation">{
class="token keyword">const fn class="token operator">= descclass="token punctuation">.value
descclass="token punctuation">.class="token function-variable function">value class="token operator">= class="token keyword">function class="token punctuation">(class="token operator">...argsclass="token punctuation">) class="token punctuation">{
class="token function">logclass="token punctuation">(class="token template-string">class="token string">`[logger] invoke class="token interpolation">class="token interpolation-punctuation punctuation">${targetclass="token punctuation">.constructorclass="token punctuation">.nameclass="token interpolation-punctuation punctuation">}class="token string">#class="token interpolation">class="token interpolation-punctuation punctuation">${nameclass="token interpolation-punctuation punctuation">}class="token string">`class="token punctuation">)
class="token keyword">return fnclass="token punctuation">.class="token function">applyclass="token punctuation">(class="token keyword">thisclass="token punctuation">, class="token operator">...argsclass="token punctuation">)
class="token punctuation">}
class="token keyword">return desc
class="token punctuation">}
class="token keyword">class class="token class-name">Counter class="token punctuation">{
count class="token operator">= class="token number">0
@logger
class="token function">incrementclass="token punctuation">(class="token punctuation">) class="token punctuation">{
class="token keyword">thisclass="token punctuation">.countclass="token operator">++
class="token keyword">thisclass="token punctuation">.class="token function">showclass="token punctuation">(class="token punctuation">)
class="token punctuation">}
@logger
class="token function">resetclass="token punctuation">(class="token punctuation">) class="token punctuation">{
class="token keyword">thisclass="token punctuation">.count class="token operator">= class="token number">0
class="token keyword">thisclass="token punctuation">.class="token function">showclass="token punctuation">(class="token punctuation">)
class="token punctuation">}
class="token function">showclass="token punctuation">(class="token punctuation">) class="token punctuation">{
class="token function">logclass="token punctuation">(class="token template-string">class="token string">`count = class="token interpolation">class="token interpolation-punctuation punctuation">${class="token keyword">thisclass="token punctuation">.countclass="token interpolation-punctuation punctuation">}class="token string">`class="token punctuation">)
class="token punctuation">}
class="token punctuation">}
class="token function">logclass="token punctuation">(class="token string">'Counter: 'class="token punctuation">, Counterclass="token punctuation">)
class="token keyword">const counter class="token operator">= class="token keyword">new class="token class-name">Counterclass="token punctuation">(class="token punctuation">)
class="token function">logclass="token punctuation">(class="token string">'counter: 'class="token punctuation">, counterclass="token punctuation">)
counterclass="token punctuation">.class="token function">incrementclass="token punctuation">(class="token punctuation">)
counterclass="token punctuation">.class="token function">incrementclass="token punctuation">(class="token punctuation">)
counterclass="token punctuation">.class="token function">incrementclass="token punctuation">(class="token punctuation">)
counterclass="token punctuation">.class="token function">resetclass="token punctuation">(class="token punctuation">)
counterclass="token punctuation">.class="token function">incrementclass="token punctuation">(class="token punctuation">)
我们透过重新定义一个 desc.value
方法,相当于是进行一层方法的代理,并在每次调用方法的时候记录(输出)操作
2.2.4 autobind 绑定实例
前面我们提过,事实上实例方法就是作为一个属性的 value
存在,也就是说我们可以把这个方法的获取改为 get
访问器属性进而实现关于调用对象或是其他的提前绑定
本节来实现一个绑定实例的装饰器
class="prism language-js">class="token keyword">import class="token punctuation">{ logclass="token punctuation">, group class="token punctuation">} class="token keyword">from class="token string">'../utils'
class="token keyword">function class="token function">bindSelfclass="token punctuation">(targetclass="token punctuation">, nameclass="token punctuation">, class="token punctuation">{ valueclass="token punctuation">: fnclass="token punctuation">, configurableclass="token punctuation">, enumerable class="token punctuation">}class="token punctuation">) class="token punctuation">{
class="token keyword">const class="token punctuation">{ constructor class="token punctuation">} class="token operator">= target
class="token keyword">return class="token punctuation">{
configurableclass="token punctuation">,
enumerableclass="token punctuation">,
class="token keyword">getclass="token punctuation">(class="token punctuation">) class="token punctuation">{
class="token function">groupclass="token punctuation">(class="token string">'in getter'class="token punctuation">, class="token punctuation">(class="token punctuation">) class="token operator">=> class="token punctuation">{
class="token function">logclass="token punctuation">(class="token template-string">class="token string">`target === A.prototype: class="token interpolation">class="token interpolation-punctuation punctuation">${target class="token operator">=== class="token constant">Aclass="token punctuation">.prototypeclass="token interpolation-punctuation punctuation">}class="token string">`class="token punctuation">)
class="token function">logclass="token punctuation">(class="token template-string">class="token string">`this === a: class="token interpolation">class="token interpolation-punctuation punctuation">${class="token keyword">this class="token operator">=== aclass="token interpolation-punctuation punctuation">}class="token string">`class="token punctuation">)
class="token punctuation">}class="token punctuation">)
class="token keyword">const boundFn class="token operator">= fnclass="token punctuation">.class="token function">bindclass="token punctuation">(class="token keyword">thisclass="token punctuation">)
class="token keyword">return boundFn
class="token punctuation">}class="token punctuation">,
class="token keyword">setclass="token punctuation">(class="token punctuation">) class="token punctuation">{class="token punctuation">}class="token punctuation">,
class="token punctuation">}
class="token punctuation">}
class="token keyword">let id class="token operator">= class="token number">0
class="token keyword">class class="token class-name">A class="token punctuation">{
id class="token operator">= idclass="token operator">++
@bindSelf
class="token function">getInstanceclass="token punctuation">(class="token punctuation">) class="token punctuation">{
class="token keyword">return class="token keyword">this
class="token punctuation">}
class="token punctuation">}
class="token keyword">const a class="token operator">= class="token keyword">new class="token class-name">Aclass="token punctuation">(class="token punctuation">)
class="token keyword">const a2 class="token operator">= class="token keyword">new class="token class-name">Aclass="token punctuation">(class="token punctuation">)
class="token keyword">const getInstance class="token operator">= aclass="token punctuation">.getInstance
class="token function">logclass="token punctuation">(class="token function">getInstanceclass="token punctuation">(class="token punctuation">)class="token punctuation">)
class="token keyword">const getInstance2 class="token operator">= a2class="token punctuation">.getInstance
class="token function">logclass="token punctuation">(class="token function">getInstance2class="token punctuation">(class="token punctuation">)class="token punctuation">)
其中最核心的就是将装饰目标方法改为 getter 并绑定访问对象 const boundFn = fn.bind(this)
,这样就使得我们在根据 a.getInstance
提取方法的时候返回的就是一个与实例绑定的方法 getInstance
in getter
target === A.prototype: true
this === a: true
A { id: 0 }
in getter
target === A.prototype: true
this === a: false
A { id: 1 }
如此一来我们就可以看到 getter 方法内部的 this 就会绑定最后访问该方法的那个实例对象
2.3 补充:类实例属性、访问器属性装饰器
最后我们补充一下对于了实例属性、访问器属性的修饰
class="prism language-js">class="token keyword">import class="token punctuation">{ logclass="token punctuation">, group class="token punctuation">} class="token keyword">from class="token string">'../utils'
class="token keyword">const class="token function-variable function">test class="token operator">=
class="token punctuation">(tagclass="token punctuation">) class="token operator">=>
class="token punctuation">(class="token operator">...argsclass="token punctuation">) class="token operator">=> class="token punctuation">{
class="token function">groupclass="token punctuation">(tagclass="token punctuation">, class="token punctuation">(class="token punctuation">) class="token operator">=> class="token punctuation">{
argsclass="token punctuation">.class="token function">forEachclass="token punctuation">(class="token punctuation">(argclass="token punctuation">, iclass="token punctuation">) class="token operator">=> class="token function">logclass="token punctuation">(iclass="token punctuation">, argclass="token punctuation">)class="token punctuation">)
class="token punctuation">}class="token punctuation">)
class="token punctuation">}
class="token keyword">class class="token class-name">A class="token punctuation">{
@class="token function">testclass="token punctuation">(class="token string">'field'class="token punctuation">)
num class="token operator">= class="token number">0
@class="token function">testclass="token punctuation">(class="token string">'accessor'class="token punctuation">)
class="token keyword">get class="token function">showclass="token punctuation">(class="token punctuation">) class="token punctuation">{
class="token function">logclass="token punctuation">(class="token template-string">class="token string">`num = class="token interpolation">class="token interpolation-punctuation punctuation">${class="token keyword">thisclass="token punctuation">.numclass="token interpolation-punctuation punctuation">}class="token string">`class="token punctuation">)
class="token punctuation">}
class="token punctuation">}
field
0 A {}
1 num
2 {
configurable: true,
enumerable: true,
writable: true,
initializer: [Function: initializer]
}
accessor
0 A {}
1 show
2 {
get: [Function: get],
set: undefined,
enumerable: false,
configurable: true
}
我们可以看到与类原型方法大同小异,主要就是属性描述符的差别
3. 特性总结
最后我们根据装饰对象的不同记录以下装饰器方法的参数和默认属性:
装饰目标 | 参数列表 | 描述符属性 |
---|
类定义(class) | (target) | class="katex--inline">class="katex">class="katex-mathml">
×
\times
class="katex-html">class="base">class="strut" style="height: 0.66666em; vertical-align: -0.08333em;">class="mord">× |
实例属性(field) | (target, name, description) | configurable: true enumerable: true writable: true initializer: [Function] |
原型方法(method) | (target, name, description) | configurable: true enumerable: false writable: true value: [Function] |
访问器属性(accessor) | (target, name, description) | configurable: true enumerable: true get: [Function] set: [Function] |
补充:使用环境配置注意事项 & 三方库应用
最后再提一点,由于前面提过的装饰器实际上还处于提案阶段(目前到 stage-2 了),实际上并不属于任何版本的现行标准,通常要真正使用的话必须配合 babel 的插件 @babel/plugin-proposal-decorators
使用,如下(使用 @babel/register
运行时启用):
class="prism language-js">class="token function">requireclass="token punctuation">(class="token string">'@babel/register'class="token punctuation">)class="token punctuation">(class="token punctuation">{
presetsclass="token punctuation">: class="token punctuation">[class="token string">'@babel/env'class="token punctuation">]class="token punctuation">,
pluginsclass="token punctuation">: class="token punctuation">[
class="token punctuation">[
class="token string">'@babel/plugin-proposal-decorators'class="token punctuation">,
class="token punctuation">{
legacyclass="token punctuation">: class="token boolean">true
class="token punctuation">}
class="token punctuation">]
class="token punctuation">]
class="token punctuation">}class="token punctuation">)
moduleclass="token punctuation">.exports class="token operator">= class="token function">requireclass="token punctuation">(class="token string">'./index.js'class="token punctuation">)
MobX
其他还有像是 MobX 在版本 6 之前有大量对于装饰器语法的实现和应用
与 core-js
相似,core-decorators
提供了许多基础常见的装饰器实现,开箱即用
结语
ES7 的 装饰器(Decorator) 特性是一个非常有趣的特性,算是一种属于语言语法层面的特性,但是却又是与其他语言(如 Java)的原生特性非常之相似,甚至具备更强大的能力,合理的使用和发挥想象力的应用模式可以创造出兼具创意和可读性、可用性高的代码应用,供大家参考。
其他资源
参考连接
完整代码示例
https://github.com/superfreeeee/Blog-code/tree/main/front_end/es6/es7_decorator