ES7 提案: Decorators 装饰器

news/2024/7/4 1:44:47 标签: class, javascript, decorators, proxy, ecmascript
class="baidu_pl">
class="article_content clearfix">
class="markdown_views prism-atom-one-dark">

ES7 提案: Decorators 装饰器

class="toc">

文章目录

  • ES7 提案: Decorators 装饰器
  • 前言
  • 正文
    • 1. Decorator 装饰器使用规范
      • 1.1 装饰器的使用形式 & 具体行为
      • 1.2 为何不能修饰函数?
    • 2. 详细说明 & 代码示例
      • 2.1 类装饰器 Class Decorators
        • 2.1.1 传入参数 & 基础用法
        • 2.1.2 装饰器返回值
        • 2.1.3 装饰器表达式
        • 2.1.4 装饰器注入原型方法
        • 2.1.5 装饰器实现混入(Mixin)模式
      • 2.2 类方法装饰器 Class Method Decorators
        • 2.2.1 传入参数 & 基础用法
        • 2.2.2 readonly 只读属性
        • 2.2.3 logger 日志装饰器
        • 2.2.4 autobind 绑定实例
      • 2.3 补充:类实例属性、访问器属性装饰器
    • 3. 特性总结
    • 补充:使用环境配置注意事项 & 三方库应用
  • 结语
  • 其他资源
    • 参考连接
    • 完整代码示例

前言

今天我们来说说一个 ES7 提出的实验性特性,截止目前为止还处于 stage-2 的 Decorators 装饰器。他的使用形式就好像 Java 里面的注解(Annotation)一样,然而其实现机制和能力却又比 Java 的标记型注解要强大许多,下面我们就来看看 ES7 装饰器的具体用法和效果。

正文

1. Decorator 装饰器使用规范

首先我们先看看装饰器的使用形态和使用范围,装饰器的表达式如下

class="prism language-js">@class="token operator"><decoratorclass="token operator">-expressionclass="token operator">>

使用 @ 符号加上一个返回一个函数的表达式

装饰器通常是用于 “修饰” 某个目标,也就是为某个目标添加一些特性,同时装饰器能够装饰的目标也被局限为下列两种

  1. 类(ES6 的 class)
  2. 类属性
    1. 类实例属性(field)
    2. 类方法(method)
    3. 类访问器属性(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_isTestableOtherClass._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 关键字后定义的方法、属性、访问器属性其实都是作为某个对象的属性(原型方法、实例属性、实例访问器属性),也就是说其实他们接受的参数类型是非常相似的:

  • src/method/mixin.js
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
}

我们可以看到针对三种目标都接受三个参数:

  1. 类定义对象
  2. 方法/实例属性/实例访问器属性名
  3. 属性描述符(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 只读属性

第一种应用我们可以定义用于类方法的只读装饰器实现

  • src/method/readonly.js
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 作为日志装饰器,记录一个对象指定方法的各个调用记录

  • src/method/logger.js
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 访问器属性进而实现关于调用对象或是其他的提前绑定

本节来实现一个绑定实例的装饰器

  • src/method/autobind.js
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 补充:类实例属性、访问器属性装饰器

最后我们补充一下对于了实例属性、访问器属性的修饰

  • src/field/basic.js
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 运行时启用):

  • register.js
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 之前有大量对于装饰器语法的实现和应用

decorators_677">core-decorators

core-js 相似,core-decorators 提供了许多基础常见的装饰器实现,开箱即用

结语

ES7 的 装饰器(Decorator) 特性是一个非常有趣的特性,算是一种属于语言语法层面的特性,但是却又是与其他语言(如 Java)的原生特性非常之相似,甚至具备更强大的能力,合理的使用和发挥想象力的应用模式可以创造出兼具创意和可读性、可用性高的代码应用,供大家参考。

其他资源

参考连接

TitleLink
ES6 修饰器http://caibaojian.com/es6/decorator.html
tc39/proposal-decorators - githubhttps://github.com/tc39/proposal-decorators
jayphelps/core-decorators - githubhttps://github.com/jayphelps/core-decorators
MobX-Enabling decoratorshttps://mobx.js.org/enabling-decorators.html

完整代码示例

https://github.com/superfreeeee/Blog-code/tree/main/front_end/es6/es7_decorator


http://www.niftyadmin.cn/n/735310.html

相关文章

Vue2 源码解析: MVVM 双向绑定3 - 模版编译实现

Vue2 源码解析: MVVM 双向绑定3 - 模版编译实现 文章目录Vue2 源码解析: MVVM 双向绑定3 - 模版编译实现前言(长文慎入&#xff01;)正文回顾&#xff1a;MVVM 全流程1. $mount 组件挂载1.1 template 获取模版1.1.1 idToTemplate 根据 id 选择器获取模版1.1.2 getOuterHTML 根据…

function深刻理解

转载于:https://www.cnblogs.com/liuliang389897172/p/8946342.html

测试人员与开发人员的比例究竟多少是合理的?

在一些软件大会上&#xff0c;人们常常会问这样一个问题&#xff1a;测试人员与开发人员的比例究竟多少是合理的&#xff1f;而这样的问题&#xff0c;很难直接给出一个答案。为什么会有这样的问题&#xff0c;可能来自于两方面的压力&#xff1a;许多公司领导总是希望得到一个…

Python_把csv文件导入数据库

涉及到两个包&#xff1a;sqlalchemy 与 pymysql 单用sqlalchemy包中的engine可以快速把当前目录下所有的csv文件导入数据库 import os import pandas as pd from sqlalchemy import create_engine#连接数据库函数 def engine(db_name):engine create_engine(mysqlpymysql:/…

用Certbot 获取和自动更新SSL证书

前言 之前写过一篇「HTTPS时代&#xff0c;免费SSL获取与配置(Apache版)」, 使用的是sslforfree.org 服务&#xff0c;这个服务比较傻瓜&#xff0c;按照步骤一步一步就可以得到证书&#xff0c;但是因为证书最长的过期时间是3个月&#xff0c;每次都需要苦逼得重新去操作一遍。…

React 入门: 核心特性全面解析

React 入门: 核心特性全面解析 文章目录React 入门: 核心特性全面解析前言正文0. 项目搭建1. JSX1.1 揭露 JSX 的神秘面纱1.2 扩展 JS(强化版的 html)1.3 ReactDOM 渲染模版2. 组件基础2.1 类组件(Class Component) vs 函数组件(Function Component)3. Props 属性/数据传递(父组…

Python 以某一列的空值删除整行

语法&#xff1a; df df[df[column].notna()] 案例&#xff1a; import numpy as np import pandas as pd df pd.DataFrame(np.random.randn(3,4)) df.iloc[:2,1] np.nannew_df df[df[1].notna()] #以此列的空值删除整行,1可以和指定列来替代的 print(new_df) print(df) …

通过案例分析MySQL中令人头疼的Aborted告警

2019独角兽企业重金招聘Python工程师标准>>> 通过案例分析MySQL中令人头疼的Aborted告警 转载 2017-06-29 作者&#xff1a;dbapower 我要评论 这篇文章通过案例跟大家分析了MySQL中令人头疼的Aborted告警的相关资料&#xff0c;文中将Aborted告警介绍的非常详…