ES7 特性 Decorator (装饰器)

605 阅读3分钟

ES6 引入了 class 特性来创建类,而在 ES7 中引入了 Decorator,方便我们对类/属性的装饰,在不改变原有对象基础上,丰富原对象的功能。该特性目前处于 stage2,并未进入稳定阶段

Decorator 只是对象/类本身的扩展(继承是对原型链扩展

本文章,简单讲讲个人对这个特性的理解,希望大家能在阅读此文章后,能对其有一定理解

装饰器模式的优缺点

优点:

1、不改变原有对象基础上,丰富原对象的功能。相比与静态类的继承,装饰器模式正如它定义的那样,可以动态的给一个对象添加额外的职责, 显得更加灵活。静态继承的情况下,如果要添加其他的功能就需要添加新的子类实现功能,然后相互之间继承,以达到一个组合的功能,对于每一个要添加的功能都要,新建类,显得特别麻烦,也使得系统越来越复杂,而对于装饰器来说,为一个特定的 Component 提供多种不同的 Decorator,对于一些要达成的功能,相互组合就可以达成目的

2、装饰器和被装饰的对象可以独立发展,而不会相互耦合

3、装饰模式是继承关系的一个替代方案。我们看装饰器 Decorator,不管装饰多少层,返回的对象还是 Component,实现的还是 is-a 的关系

缺点:

多层装饰比较复杂

为什么会复杂呢?如果装饰器使用出现问题,就像剥洋葱一样,剥到了最后才发现是最里层的装饰出现了问题,想象一下工作量吧,因此,尽量减少装饰类的数量,以便降低系统的复杂度

作用在方法上的 decorator

// ES6 decorator, effect on class method
// notice here `target` is `Cat.prototype`
function readonly(
  target: any,
  propertyKey: string,
  descriptor: PropertyDescriptor
) {
  descriptor.writable = false
  return descriptor
}

// class Cat {
//   bark() {
//     return 'miao!miao!'
//   }
// }

class Cat {
  @readonly
  bark() {
    return 'miao!miao!'
  }
}

let cat = new Cat()
try {
  cat.bark = function () {
    console.log('wang!wang!')
    return 'wang!wang!'
  }
} catch (e) {
  console.log(e);
  // here e, TypeError: Cannot assign to read only property 'bark' of object '#<Cat>'
}

原因是我们定义了 Cat 类的 bark 方法为只读属性,当我们尝试修改 'bark' 方法时,是不被允许的

通过原型链解释装饰器模式

在 ECAMScript 中 Class 实际上是语法糖,本质上还是通过原型链去实现的,我们可以看看上面代码中,被编译成 JavaScript 后,发生了什么

// use prototype to explain the decorator effect on method 
// step 1
function Cat() {}

// without decorator, step 2
Object.defineProperty(Cat.prototype, 'bark', {
  value: function () {
    return 'miao!miao!'
  },
  enumerable: false,
  configurable: true,
  writable: true,
})

而装饰器 readonly 通过对 descriptor 做修改,最后使用 Object.defineProperty 重新定义 descriptor

// with decorator, step2
let descriptor: PropertyDescriptor = {
  value: function () {
    return 'miao!miao!'
  },
  enumerable: false,
  configurable: true,
  writable: true,
}

function readonly(
  target: any,
  propertyKey: string,
  descriptor: PropertyDescriptor
): PropertyDescriptor {
  descriptor.writable = false
  return descriptor
}

// The params received by decorator are the same as Object.defineProperty
descriptor = readonly(Cat.prototype, 'bark', descriptor) || descriptor
Object.defineProperty(Cat.prototype, 'bark', descriptor)
// try modify bark
// @ts-ignore
let cat = new Cat()
try {
  cat.bark = function () {
    console.log('wang!wang!')
    return 'wang!wang!'
  }
} catch (e) {
  console.log(e);
  // here e, TypeError: Cannot assign to read only property 'bark' of object '#<Cat>'
}

作用在类上的 decorator

// decorator can effect on class, decorator's effect can not be extends
function animal(target: any) {
  target.isAnimal = true
}

// decorator can be combined with closure, and then wrapped to factory mode to use a effect on class
function doge(value: boolean) {
  return function(target: any) {
    target.isDoge = value
  }
}

@animal
@doge(false)
class Cat {}

const cat = new Cat()

// @ts-ignore
console.log(Cat.isAnimal, Cat.isDoge, cat.isAnimal) // true false undefined

@doge(true)
class Dog {}

const dog = new Dog()

// @ts-ignore
console.log(Dog.isDoge, dog.isDoge) // true undefined

实战

使用 decorator 实现捕获错误

/**
 * Catch errors in mobx actions and display corresponding error messages.
 * @example @catchAction(errorMap, defaultErrorCode)
 * @param errorMap A map of error codes and their corresponding error messages.
 * @param defaultErrorCode The default error code to be used if no specific error code is found.
 * @returns A function decorator that catches errors in the decorated function.
 */
const catchError =
  <ErrorMap extends Record<string, string>, DefaultErrorCode>(
    errorMap: ErrorMap,
    defaultErrorCode?: DefaultErrorCode,
  ) =>
  <T, U extends IArguments[]>(target: T, propertyKey: string, descriptor: PropertyDescriptor): void => {
    if (!(descriptor?.value && errorMap)) return;

    const originalFn: (...args: U) => Promise<any> = descriptor.value;
    descriptor.value = async function decoratedFunc(...args: U): Promise<any> {
      try {
        const result = await originalFn.apply(this, args);
        return result;
      } catch (error) {
        const code = _get(error, 'response.data.code');
        const errorMessage = errorMap[String(code)] || errorMap[String(defaultErrorCode)];
        if (errorMessage) alert(errorMessage);
        Promise.reject(error);
        return error;
      }
    };
  };

使用场景

装饰器模式在前端开发中广泛使用,常见的应用场景如:埋点、路由、性能监控、日志系统等。React 的高阶组件(HOC)以及 Redux、Mobx 等库中也有广泛使用