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 等库中也有广泛使用