Decorator (装饰器) 实现原理及其使用
定义
装饰器 (Decorator) 是 ES7 中的一个提案, 目前仍处于 stage-2: 草稿状态。它是一种与类 (class) 相关的语法, 用来注释或修改类和类方法。装饰器是一种函数, 写成@ + 函数名, 它可以放在类和类方法的定义前面。
作用
它们不仅增加了代码的可读性, 清晰地表达了意图, 而且提供一种方便的手段, 增加或修改类的功能。
使用
前提
由于目前原生的JavaScript还不支持装饰器, 所以, 我们需要借助 babel 的能力来实现。
- 安装
babel以及decorator插件
npm install @babel/cli @babel/core --save-dev
npm install @babel/proposal-decorators @babel/proposal-class-properties --save-dev
- 配置
.babelrc
"plugins": [
["@babel/proposal-decorators", {
"legency": true
}],
["@babel/proposal-class-properties", {
"loose": true
}]
]
当然你也可以到这里看 babel 编译结果 I'm here !!
类装饰器
function decorator(target, key, descriptor) {
// target -> 所要装饰的目标类, Foo 类
// key -> undefined
// descriptor -> undefined
}
@decorator
class Foo {}
// ===>
Foo = decorator(Foo) || Foo
装饰器对类的行为的改变, 是代码编译时发生的, 而不是在运行时。这意味着, 装饰器能在编译阶段运行代码。也就是说, 装饰器本质就是编译时执行的函数。
类方法, 类属性装饰器
class Person {
@readonly
name() { return `${this.first} ${this.last}` }
}
function readonly(target, name, descriptor) {
// descriptor对象原来的值如下
// {
// value: specifiedFunction,
// enumerable: false,
// configurable: true,
// writable: true
// };
descriptor.writable = false;
return descriptor;
}
readonly(Person.prototype, 'name', descriptor);
// 类似于
Object.defineProperty(Person.prototype, 'name', descriptor);
函数方法的装饰
装饰器只能用于类和类的方法, 不能用于函数, 因为存在函数提升。如果一定要装饰函数, 可以采用高阶函数的形式直接执行。
function doSomething(name) {
console.log('Hello, ' + name);
}
function loggingDecorator(wrapped) {
return function() {
console.log('Starting');
const result = wrapped.apply(this, arguments);
console.log('Finished');
return result;
}
}
const wrapped = loggingDecorator(doSomething);
执行顺序
- 属性装饰器
- 方法装饰器
- 方法参数装饰器: 从后往前
function Test(
// 后执行
@Param1() name,
// 先执行
@Param2() age,
) {
// ...
} - 类装饰器: 从后往前
// 后执行
@decorator1()
// 先执行
@decorator2()
class Test {
// ...
}
实现原理
装饰器实际是一种编译时执行的函数, 它的实现依赖于 JavaScript的 Object.defineProperty。
Object.defineProperty(obj, prop, descriptor)
参数
obj要定义属性的对象。
prop要定义或修改的属性的名称或
Symbol。descriptor要定义或修改的属性描述符。
configurable当且仅当该属性的
configurable键值为true时, 该属性的描述符才能够被改变, 同时该属性也能从对应的对象上被删除。 默认为 false。enumerable当且仅当该属性的
enumerable键值为true时, 该属性才会出现在对象的枚举属性中。 默认为 false。
数据描述符还具有以下可选键值:
value该属性对应的值。可以是任何有效的
JavaScript值(数值, 对象, 函数等)。 默认为 undefined。writable当且仅当该属性的
writable键值为true时, 属性的值, 也就是上面的value, 才能被赋值运算符改变。 默认为 false。
存取描述符还具有以下可选键值:
get属性的
getter函数, 如果没有getter, 则为undefined。当访问该属性时, 会调用此函数。执行时不传入任何参数, 但是会传入this对象(由于继承关系, 这里的this并不一定是定义该属性的对象)。该函数的返回值会被用作属性的值。 默认为 undefined。set属性的
setter函数, 如果没有setter, 则为undefined。当属性值被修改时, 会调用此函数。该方法接受一个参数(也就是被赋予的新值), 会传入赋值时的this对象。 默认为 undefined。
babel 转换
我们通过 babel 转换的结果来理解 decorator 是如何实现的。
编译前
class Foo {
@readonly
get name() {
return 'Rain120';
}
}
function readonly(target, name, descriptor) {
return descriptor;
}
编译后
var _class;
function _applyDecoratedDescriptor(
target, // _class.prototype,
property, // "name",
decorators, // [readonly],
descriptor, // Object.getOwnPropertyDescriptor(_class.prototype, "name"),
context // _class.prototype
) {
var desc = {};
// 拷贝属性
Object.keys(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;
}
// 应用多个 decorators
desc = decorators.slice().reverse().reduce(function (desc, decorator) {
return decorator(target, property, desc) || desc;
}, desc);
// 设置 decorators 修改的属性
if (context && desc.initializer !== void 0) {
desc.value = desc.initializer ? desc.initializer.call(context) : void 0;
desc.initializer = undefined;
}
if (desc.initializer === void 0) {
Object.defineProperty(target, property, desc);
desc = null;
}
return desc;
}
function readonly(target, key, descriptor) {
return descriptor;
}
let Foo = (
_class = class Foo {
get name() {
return 'Rain120';
}
},
_applyDecoratedDescriptor(
_class.prototype,
"name", [readonly],
Object.getOwnPropertyDescriptor(_class.prototype, "name"),
_class.prototype
),
_class
);
自己实现
Decorator装饰方法时,descriptor的参数和Object.defineProperty的descriptor一致Decorator装饰类属性时,descriptor没有value和get或set, 且多出一个initializer方法, 返回值作为属性的值
无参的 decorator
/**
* @description 装饰器函数
* @param target 被装饰器装饰的目标对象原型
* @param key 要定义或修改的属性, 类和方法的名称或Symbol
* @param descriptor 要定义或修改的属性, 类和方法的描述符
*/
function Decorator(target, key, descriptor) {
let descriptorValue = descriptor.initializer && descriptor.initializer.call(this);
const descriptor = {
enumerable: false,
configurable: true,
writable: true,
get() {
return descriptorValue;
},
set(value) {
descriptorValue = value;
}
};
return descriptor;
}
带参的 decorator
/**
* @description 装饰器函数
* @param target 被装饰器装饰的目标对象原型
* @param key 要定义或修改的属性, 类和方法的名称或Symbol
* @param descriptor 要定义或修改的属性, 类和方法的描述符
*/
function Decorator(props) {
return (target, key, descriptor) => {
let descriptorValue = descriptor.initializer && descriptor.initializer.call(this);
const descriptor = {
enumerable: false,
configurable: true,
writable: true,
get() {
return descriptorValue;
},
set(value) {
descriptorValue = value;
}
};
return descriptor;
}
}