什么是装饰器模式?
装饰器模式(Decorator Pattern)允许向一个现有的对象添加新的功能,同时又不改变其结构。这种类型的设计模式属于结构型模式,它是作为现有的类的一个包装。
在 JavaScript 中,在 ES5 时代,由于没有 class 语法糖,ES5 的面向对象是用 function 函数模拟出来的,所以那个时候的装饰器模式更像是一种高阶函数模式,用来增强原来的函数。
const log = (srcFun) => {
if(typeof(srcFun) !== 'function') {
throw new Error(`the param must be a function`);
}
return (...arguments) => {
console.info(`${srcFun.name} invoke with ${arguments.join(',')}`);
srcFun(...arguments);
}
}
const plus = (a, b) => a + b;
const logPlus = log(plus);
logPlus(1,2); // this will log : plus invoke with 1,2
ES6 的出现带来了 class 语法糖,ES7 带来了 Decorator 语法,这让 JavaScript 的装饰器模式更加接近其他语言的语法:
const log = (target, name, descriptor) => {
var oldValue = descriptor.value;
descriptor.value = function() {
console.log(`Calling ${name} with`, arguments);
return oldValue.apply(this, arguments);
};
return descriptor;
}
class Math {
@log // Decorator
plus(a, b) {
return a + b;
}
}
const math = new Math();
math.add(1, 2); // this will log: Calling plus with 1,2
Decorator 语法的作用场景有两种:
当一个装饰器作用于类的时候,是这个样子的:
function isAnimal(target) {
target.isAnimal = true;
return target;
}
@isAnimal
class Cat {
...
}
console.log(Cat.isAnimal); // true
当一个装饰器作用于类属性的时候,是这个样子的:
function readonly(target, name, descriptor) {
discriptor.writable = false;
return discriptor;
}
class Cat {
@readonly
say() {
console.log("meow ~");
}
}
var kitty = new Cat();
kitty.say = function() {
console.log("woof !");
}
kitty.say() // meow ~
Decorator 语法的实质是 Object.defineProperty 的语法糖,所以其实装饰器在起作用的时候,实际上是通过 Object.defineProperty 来进行扩展和封装的。
let descriptor = {
value: function() {
console.log("meow ~");
},
enumerable: false,
configurable: true,
writable: true
};
descriptor = readonly(Cat.prototype, "say", descriptor) || descriptor;
Object.defineProperty(Cat.prototype, "say", descriptor);
所以,我们可以进一步得出:
当装饰器作用于类本身的时候
当装饰器作用于类的某个具体的属性的时候
目前使用 Decorator 会有一个问题,那就是 class 中的箭头函数跟普通函数用装饰器会有不一样的效果。
function test(target, name, descriptor) {
console.log(target, name, descriptor)
return descriptor
}
class Cat {
@test
say = () => {}
@test
say() {}
}
具体表现来说:
常规类方法:
属性方法:
原因在于,装饰器的执行是在类创建后(实例生成前),这里就发生了一个概念上的小冲突,装饰器执行时属性方法似乎还没创建。
当然,为了弥合这个问题,babel 工具会在编译的时候就做好了兼容,这就是给属性方法添加一个 initializer,具体细节可以见此文,或者研究 babel 编译过程。
总之,Decorator 目前还不算特别成熟,实际应用中不同的实现会有差异,以观后效吧。
实现一个兼容普通类函数和类属性函数的装饰器,并且保留箭头函数的 this 绑定。
function test() {
return function (target, name, descriptor) {
// 类属性方法
if (descriptor.initializer) {
const originInitializer = descriptor.initializer
descriptor.initializer = function() {
// 执行 initializer 会返回这个原始的属性方法
const fn = originInitializer.call(this)
return function(...args) {
// 做其他的工作
...
// 调用原来的方法
fn.call(this, ...args)
}
}
} else {
// 常规方法
const originValue = descriptor.value;
descriptor.value = function(...args) {
// 做其他的工作
...
// 将 this 绑定
originValue.call(this, ...args)
};
return descriptor;
}
}
}
class Cat {
@test()
say = () => {
console.log(this)
}
@test()
hello() {
console.log(this)
}
}