-
Notifications
You must be signed in to change notification settings - Fork 11
Description
什么是装饰器(Decorator)?
在设计模式中,装饰器是指在不改变原有类的成员方法下,通过一个包装对象来动态扩展原对象的功能。用通俗的话语来讲,它就像是一个画框,无论你是什么样的画,装上了画框就有了保护画的功能与挂上墙壁的功能。
而在其他很多语言包括 Java,Python,都有装饰器的语法,形如:
class Person {
@readonly
walk() {}
}
可以看到,通过 @
这个字符,使用了 readonly 装饰器,非常方便地使得 Person 类的 walk 方法变成了一个只读的方法。而在 ES7 语法中,也同样拥有类似的语法,它不仅可以作用于类的成员上,还可以作用于类本身上面:
@testable
class Person {
constructor(){}
}
function testable(target) {
target.test = true;
}
这里例子中直接利用了 testable 这一个装饰器,给 Person 这个类加上了一个 test 的属性。
可以看到装饰器可以动态地为 Person 这个类加上了一些特殊的标识,而且装饰器可以复用到很多不同的类中。
装饰器的原理是什么?
实际上,在 EcmaScript 中,装饰器语法是依赖于 Object.defineProperty 这个 API 的,甚至可以说装饰器是它的一个语法糖。
装饰器是代码编译期间对类或类成员发生改变,并不是运行时,我们来看一下 babel 转义出来的代码:
function _applyDecoratedDescriptor(target, property, decorators, descriptor, context) {
// 接收装饰器数组 decorators
var desc = {};
Object['ke' + 'ys'](descriptor).forEach(function (key) {
desc[key] = descriptor[key];
});
desc.enumerable = !!desc.enumerable;
desc.configurable = !!desc.configurable;
if ('value' in desc || desc.initializer) {
desc.writable = true;
}
// 批量执行装饰器函数
desc = decorators.slice().reverse().reduce(function (desc, decorator) {
return decorator(target, property, desc) || desc;
}, desc);
if (context && desc.initializer !== void 0) {
desc.value = desc.initializer ? desc.initializer.call(context) : void 0;
desc.initializer = undefined;
}
//使用 Object.defineProperty 来重新定义类的成员
if (desc.initializer === void 0) {
Object['define' + 'Property'](target, property, desc);
desc = null;
}
return desc;
}
// 调用方式
_applyDecoratedDescriptor(_class2.prototype, 'key', [decorator], Object.getOwnPropertyDescriptor(_class2.prototype, 'key'), _class2.prototype)
如果这个装饰器是直接作用在类上面的,那么就像之前所说的那样,翻译代码如:
class _Person {
constructor(){}
}
const Person = decorator(_Person) || _Person;
从上面的代码可以看出 decorator 确实是在代码编译阶段就执行的,并且翻译后依赖于 Object.defineProperty 这个 API,实现了对于原成员的重新定义与包装。
装饰器本质上是一个函数,它接收的参数形式都是固定的,也就是 Object.defineProperty 函数的参数,第一个参数是作用的对象,第二个参数是需要重新定义的对应的键名,第三个参数是属性描述对象,对象包括 writable, configurable, enumerable 等参数,详细的参数可以看 MDN 文档,这里不再累述。
装饰器怎么用?
装饰器可以作用在类以及类的成员上面。如果直接作用在类上面,那么装饰器函数中只有一个参数,如下:
@color('white')
class Cat {
constructor(name) {
this.name = name;
}
}
function color(style) {
return function (target) {
target.prototype.color = style;
}
}
const cat = new Cat('tom');
console.log(cat.color); // 'white'
color 函数的返回值是一个函数,返回的这个函数会在编译阶段就被执行,执行后函数传入的 target 也就是这里的 Cat 类会被添加上一个 color 属性在原型上,值为 'white'。
如果是作用在类的成员上的话,那么它回调函数中的参数就会如同 Object.defineProperty 函数一样,如下:
class Person {
name = createName();
constructor() {}
@withWho('girlFriend')
walk() {
console.log('I \'m walking.');
}
}
function withWho (name) {
return function (target, key, descriptor) {
console.log(`with ${name}`);
console.log(target);
console.log(key);
console.log(descriptor);
const func = descriptor.value; // 通过 value 获取
const editedFunc = function () {
console.log(`with ${name}`);
return func();
}
descriptor.value = editedFunc;
}
}
const jack = new Person();
jack.walk();
// with girlFriend
// I 'm walking
上面的例子通过 withWho 这个装饰器,实现了对 Person 类的 walk 方法的包裹,后续如果调用 walk 方法,都相当于执行装饰器包裹后的 editedFunc 函数的逻辑。
使用场景有哪些?
装饰器的作用很多,可以高度复用于很多类上与函数上,并且具有注释的功能,像文章最开始的代码,一看就知道 Person 类是具有可供测试的特性的。下面,我将列出一些常用的修饰器函数:
有哪些常见的装饰器?
readonly
function readonly(target, key, descriptor) {
descriptor.writable = false;
return descriptor;
}
autobind
首先 autobind 分为 4 种情况:
- 直接读取对应 autobind 的方法,即方法定义在 Class Father 上面,直接通过 Father.prototype.xxx 来读取,此时直接返回对应的方法,不做 bind。
- autobind 作用的方法定义在 Father 类上面,然后 Son 子类继承于父类即 class Son extends Father,然后直接通过 Son.prototype.xxx 来读取方法,此时 this.constructor(Son) 是不同于原本的 construtor (Father),直接返回函数。
- Autobind 作用的函数在 this 对象的原型链上,此时不能去修改 descriptor,因为这是公用的 class 上面的 descriptor,所以需要每个对象重新绑定一次,为了避免重复绑定造成内存泄漏,所以这里使用 weekset 来存储已经绑定过的函数。
- 除了 1,2,3 种情况,就是直接在实例对象上面读取,那么直接就使用 bind 绑定对应的函数,使用 Object.defineProperty 来重新定义对应对象的 key。
let store;
function getBoundSuper(context, fn) {
if (typeof WeekSet === 'undefined') {
throw new Error(`WeekSet must suppoerted`);
}
if(!store) {
store = new WeekSet();
}
if (!store.has(context)) {
store.set(context, new WeekSet());
}
const contextStore = store.get(context);
if (!contextStore.has(fn)) {
contextStore.set(fn, fn.bind(context));
}
return contextStore.get(fn);
}
function autobind(target, key, descriptor) {
const { constructor } = target;
const { value, configurable, enumerable } = descriptor;
const func = value;
if (typeof func !== 'function') {
throw new SyntaxError('@autobind can only use on functions');
}
return {
configurable,
enumrable,
get() {
if (this === target) {
return this;
}
if (this.constructor !== constructor && Object.getPrototypeOf(this).constructor === constructor) {
return func;
}
if (this.constructor !== constructor && key in this.constructor.prototype) {
return getBoundSuper(this, func);
}
const bindFunc = value.bind(this);
Object.defineProperty(target, key, {
configurable,
enumerable,
value: bindFunc,
writable: true
});
return bindFunc;
}
};
}
deprecate
function deprecate(...args) {
const [msg = 'This function will be deprecate in future.'] = args;
return function (target, key, descriptor) {
if (typeof descriptor.value !== 'function') {
throw new SyntaxError('only functions can be marked as deprecated.');
}
const deprecateSignal = `${target.constructor.name}#${key}`;
return {
...descriptor,
value: function deprecateWrap() {
console.warn(`[DEPRECATION]: ${deprecateSignal} ${msg}`);
return descriptor.value.apply(this, arguments);
}
};
}
}
lazyInitialize
function lazyInitialize (target, key, descriptor) {
const { configurable, enumerable, initialize, value } = descriptor;
Object.defineProperty(target, key, {
get() {
const ret = initiallize ? initialize.call(this): value;
Object.defineProperty(target, key, {
configurable,
enumerable,
value: ret,
writable: true
});
return ret;
},
configurable,
enumerable
});
}
当使用到该属性的时候,才去初始化对应属性的值,并且只初始化一次。只有当 lazyInitialize 修饰的是类的属性的时候,才会有 initialize 这个 descriptor 属性。