ES6系列之装饰器
# 一、简介
装饰器依赖于
ES5的Object.defineProperty方法
# 1.1 Object.defineProperty
Object.defineProperty()方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象- 该方法允许精确添加或修改对象的属性。通过赋值来添加的普通属性会创建在属性枚举期间显示的属性(
for...in或Object.keys方法), 这些值可以被改变,也可以被删除。这种方法允许这些额外的细节从默认值改变。默认情况下,使用Object.defineProperty()添加的属性值是不可变的
Object.defineProperty(obj, prop, descriptor)
obj:要在其上定义属性的对象。prop:要定义或修改的属性的名称。descriptor:将被定义或修改的属性描述符。- 返回值:被传递给函数的对象。
在
ES6中,由于Symbol类型 的特殊性,用Symbol类型 的值来做对象的key与常规的定义或修改不同,而Object.defineProperty是定义key为Symbol的属性的方法之一
descriptor属性描述符
对象里目前存在的属性描述符有两种主要形式:数据描述符和存取描述符
- 数据描述符是一个具有值的属性,该值可能是可写的,也可能不是可写的。
- 存取描述符是由
getter-setter函数对描述的属性。
configurable
当且仅当该属性的
configurable为true时,该属性描述符才能够被改变,同时该属性也能从对应的对象上被删除。默认为false
enumerable
enumerable定义了对象的属性是否可以在for...in循环和Object.keys()中被枚举。- 当且仅当该属性的
enumerable为true时,该属性才能够出现在对象的枚举属性中。默认为false。
# 二、Babel
安装编译
npm install --save-dev @babel/core @babel/cli
npm install --save-dev @babel/plugin-proposal-decorators @babel/plugin-proposal-class-properties
新建
.babelrc文件
{
"plugins": [
["@babel/plugin-proposal-decorators", { "legacy": true }],
["@babel/plugin-proposal-class-properties", {"loose": true}]
]
}
再编译指定的文件
babel decorator.js --out-file decorator-compiled.js
# 三、用法
装饰器主要用于
- 装饰类
- 装饰方法或属性
# 3.1 类的装饰
@testable
class MyTestableClass {
// ...
}
function testable(target) {
target.isTestable = true;
}
MyTestableClass.isTestable // true
上面代码中,
@testable就是一个装饰器。它修改了MyTestableClass这 个类的行为,为它加上了静态属性isTestable。testable函数的参数target是MyTestableClass类本身
基本上,装饰器的行为就是下面这样
@decorator
class A {}
// 等同于
class A {}
A = decorator(A) || A;
也就是说,装饰器是一个对类进行处理的函数。装饰器函数的第一个参数,就是所要装饰的目标类
如果觉得一个参数不够用,可以在装饰器外面再封装一层函数
function testable(isTestable) {
return function(target) {
target.isTestable = isTestable;
}
}
@testable(true)
class MyTestableClass {}
MyTestableClass.isTestable // true
@testable(false)
class MyClass {}
MyClass.isTestable // false
上面代码中,装饰器
testable可以接受参数,这就等于可以修改装饰器的行为
注意,装饰器对类的行为的改变,是代码编译时发生的,而不是在运行时。这意味着,装饰器能在编译阶段运行代码。也就是说,装饰器本质就是编译时执行的函数。
前面的例子是为类添加一个静态属性,如果想添加实例属性,可以通过目标类的
prototype对象操作
// mixins.js
export function mixins(...list) {
return function (target) {
Object.assign(target.prototype, ...list)
}
}
// main.js
import { mixins } from './mixins'
const Foo = {
foo() { console.log('foo') }
};
@mixins(Foo)
class MyClass {}
let obj = new MyClass();
obj.foo() // 'foo'
上面代码通过装饰器
mixins,把Foo对象的方法添加到了MyClass的实例上面
# 3.2 方法的装饰
装饰器不仅可以装饰类,还可以装饰类的属性
class Person {
// 装饰器 readonly 用来装饰“类”的name方法。
@readonly
name() { return `${this.first} ${this.last}` }
}
装饰器函数
readonly一共可以接受三个参数。
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);
- 第一个参数是类的原型对象,上例是
Person.prototype,装饰器的本意是要“装饰”类的实例,但是这个时候实例还没生成,所以只能去装饰原型(这不同于类的装饰,那种情况时target参数指的是类本身) - 第二个参数是 所要装饰的属性名
- 第三个参数是 该属性的描述对象
# 3.3 函数方法的装饰
- 装饰器只能用于类和类的方法,不能用于函数,因为存在函数提升
- 另一方面,如果一定要装饰函数,可以采用高阶函数的形式直接执行
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);
# 四、使用场景
# 4.1 装饰器有注释的作用
@testable
class Person {
@readonly
@nonenumerable
name() { return `${this.first} ${this.last}` }
}
从上面代码中,我们一眼就能看出,
Person类是可测试的,而name方法是只读和不可枚举的
# 4.2 React 的 connect
实际开发中,React 与 Redux 库结合使用时,常常需要写成下面这样
class MyReactComponent extends React.Component {}
export default connect(mapStateToProps, mapDispatchToProps)(MyReactComponent);
有了装饰器,就可以改写上面的代码。装饰
@connect(mapStateToProps, mapDispatchToProps)
export default class MyReactComponent extends React.Component {}
# 4.3 loading
在
React项目中,我们可能需要在向后台请求数据时,页面出现loading动画。这个时候,你就可以使用装饰器,优雅地实现功能。
@autobind
@loadingWrap(true)
async handleSelect(params) {
await this.props.dispatch({
type: 'product_list/setQuerypParams',
querypParams: params
});
}
loadingWrap 函数如下:
export function loadingWrap(needHide) {
const defaultLoading = (
<div className="toast-loading">
<Loading className="loading-icon"/>
<div>加载中...</div>
</div>
);
return function (target, property, descriptor) {
const raw = descriptor.value;
descriptor.value = function (...args) {
Toast.info(text || defaultLoading, 0, null, true);
const res = raw.apply(this, args);
if (needHide) {
if (get('finally')(res)) {
res.finally(() => {
Toast.hide();
});
} else {
Toast.hide();
}
}
};
return descriptor;
};
}
# 4.4 log
为一个方法添加 log 函数,检查输入的参数:
# 4.5 autobind
我们很容易想到的一个场景是 React 绑定事件的时候
我们来写这样一个
autobind函数: