装饰器语法糖背后的故事

所谓语法糖,往往意味着“美好的表象”。正如 class 语法糖背后是大家早已十分熟悉的 ES5 构造函数一样,装饰器语法糖背后也是我们的老朋友,不信我们一起来看看@decorator都帮我们做了些什么:

Part1:函数传参&调用

上一节我们使用 ES6 实现装饰器模式时曾经将按钮实例传给了 Decorator,以便于后续 Decorator 可以对它进行逻辑的拓展。这也正是装饰器的最最基本操作——定义装饰器函数,将被装饰者“交给”装饰器。这也正是装饰器语法糖首先帮我们做掉的工作 —— 函数传参&调用。

类装饰器的参数

当我们给一个类添加装饰器时:

  1. function classDecorator(target) {
  2. target.hasDecorator = true
  3. return target
  4. }
  5. // 将装饰器“安装”到Button类上
  6. @classDecorator
  7. class Button {
  8. // Button类的相关逻辑
  9. }

此处的 target 就是被装饰的类本身。

方法装饰器的参数

而当我们给一个方法添加装饰器时:

  1. function funcDecorator(target, name, descriptor) {
  2. let originalMethod = descriptor.value
  3. descriptor.value = function() {
  4. console.log('我是Func的装饰器逻辑')
  5. return originalMethod.apply(this, arguments)
  6. }
  7. return descriptor
  8. }
  9. class Button {
  10. @funcDecorator
  11. onClick() {
  12. console.log('我是Func的原有逻辑')
  13. }
  14. }

此处的 target 变成了Button.prototype,即类的原型对象。这是因为 onClick 方法总是要依附其实例存在的,修饰 onClik 其实是修饰它的实例。但我们的装饰器函数执行的时候,Button 实例还并不存在。为了确保实例生成后可以顺利调用被装饰好的方法,装饰器只能去修饰 Button 类的原型对象。

装饰器函数调用的时机

装饰器函数执行的时候,Button 实例还并不存在。这是因为实例是在我们的代码运行时动态生成的,而装饰器函数则是在编译阶段就执行了。所以说装饰器函数真正能触及到的,就只有类这个层面上的对象。

Part2:将“属性描述对象”交到你手里

在编写类装饰器时,我们一般获取一个target参数就足够了。但在编写方法装饰器时,我们往往需要至少三个参数:

  1. function funcDecorator(target, name, descriptor) {
  2. let originalMethod = descriptor.value
  3. descriptor.value = function() {
  4. console.log('我是Func的装饰器逻辑')
  5. return originalMethod.apply(this, arguments)
  6. }
  7. return descriptor
  8. }

第一个参数的意义,前文已经解释过。第二个参 数name,是我们修饰的目标属性属性名,也没啥好讲的。关键就在这个 descriptor 身上,它也是我们使用频率最高的一个参数,它的真面目就是“属性描述对象”(attributes object)。这个名字大家可能不熟悉,但Object.defineProperty方法我想大家多少都用过,它的调用方式是这样的:

  1. Object.defineProperty(obj, prop, descriptor)

此处的descriptor和装饰器函数里的 descriptor 是一个东西,它是 JavaScript 提供的一个内部数据结构、一个对象,专门用来描述对象的属性。它由各种各样的属性描述符组成,这些描述符又分为数据描述符和存取描述符:

  • 数据描述符:包括 value(存放属性值,默认为默认为 undefined)、writable(表示属性值是否可改变,默认为true)、enumerable(表示属性是否可枚举,默认为 true)、configurable(属性是否可配置,默认为true)。
  • 存取描述符:包括 get 方法(访问属性时调用的方法,默认为 undefined),set(设置属性时调用的方法,默认为 undefined )

很明显,拿到了 descriptor,就相当于拿到了目标方法的控制权。通过修改 descriptor,我们就可以对目标方法为所欲为的逻辑进行拓展了~
在上文的示例中,我们通过 descriptor 获取到了原函数的函数体(originalMethod),把原函数推迟到了新逻辑(console)的后面去执行。这种做法和我们上一节在ES5中实现装饰器模式时做的事情一模一样,所以说装饰器就是这么回事儿,换汤不换药~