更多文章请查看Github博客更新2020.09.23
最近没关注装修者的建议。 我刚刚意识到新提案已经发布三年了,它们与旧提案不兼容。 这几乎是重画。
Babel 7.1.0 终于支持新的装饰器提案:您可以使用 @babel/plugin-proposal-decorators 插件提前尝试此功能。
相关历史
装饰器的概念是由 Yehuda Katz 在三年多前首次提出的。 TypeScript 在 1.5 版(2015 年)中发布了对装饰器的支持以及许多与 ES6 相关的功能。 Angular 和 MobX 等一些主流框架开始使用它们来降低开发人员的体验:这使得装饰器特别受欢迎,并给社区带来了一种错误的稳定感。
Babel 首先在 v5 中实现了装饰器,但由于提案仍在不断变化,它们在 Babel v6 中被删除。 Logan Smyth 创建了一个非官方插件(babel-plugin-transform-decorators-legacy),它继承了 Babel 5 装饰器的行为; 在 Babel 7 的 alpha 版本发布期间,该库已移至官方 Babel 存储库。 当时该插件仍在使用旧的装饰器语法,因为新的分辨率仍然不清楚。
此后,丹尼尔·埃伦伯格、布莱恩·特尔森和耶胡达·卡茨共同起草了该法案,该法案几乎被完全重新起草。 当然,并不是所有事情都已解决,因为尚未出现符合规范的实现。
Babel 7.0.0 为 @babel/plugin-proposal-decorators 插件引入了一个新标志:legacy 选项,其唯一有效值为 true。 这一突破性的变化对于为该法案从第一阶段顺利过渡到当前阶段铺平道路是必要的。
在 Babel 7.1.0 中,我们引入了对这个新提案的支持,并且在使用 @babel/plugin-proposal-decorators 插件时默认启用它。 在Babel 7.0.0中,如果我们不设置legacy: true选项,则默认无法使用该语义(相当于legacy: false)。
新提案支持使用装饰器的私有数组(私有字段)和私有方法(私有方法)。 我们还没有在 Babel 中实现这个功能(在每个类中使用装饰器或私有元素),但我们很快就会实现。
装饰器函数相关参数
新提案提出的第三个重要变化与传递给装饰器函数的参数有关。
在提案的第一个版本中,类元素装饰器接收目标类(或对象)、键和属性描述符作为参数 - 类似于传递给 Object.defineProperty 的方式。 类装饰器将目标构造函数作为唯一的参数。
新的装饰器提案越来越强大:元素装饰器将接收一个对象,该对象除了可以修改属性描述符之外还允许修改键值,该属性可以是形式参数(静态,原型或自己的),以及类型元素(字段或方法)的。 他们还可以在装饰器类上创建附加属性并定义运行函数(完成器)。
类装饰器接收包含类描述符的对象,使得类可以在创建之前对其进行更改。
参考
* * * 分向線* * *
在ES6中,减少了对类对象(例如class和extends)的相关定义和操作,这使得当我们在多个不同的类之间共享或扩展一些方法或行为时,变得不那么优雅。 这个时候,我们需要一种更加崇高的方式来帮助我们完成这种事情。
什么是装饰器Python的装饰器
在面向对象(OOP)设计模式中,装饰器被称为装饰模式。 OOP的装饰模式需要通过继承和组合来实现,而Python不仅可以支持OOP的装饰器,还可以直接从句子层面支持装饰器。
如果你熟悉python,那么你一定对它很熟悉。 那么我们来看看python中的装饰器是什么样子的:
def decorator(f):
print "my decorator"
return f
@decorator
def myfunc():
print "my function"
myfunc()
# my decorator
# my function
这里的@decorator就是我们所说的装饰器。 在前面的代码中,我们在执行我们的目标方法之前使用装饰器复制了一行文本,并且没有对原始方法进行任何更改。 代码基本上相当于:
def decorator(f):
def wrapper():
print "my decorator"
return f()
return wrapper
def myfunc():
print "my function"
myfunc = decorator(myfuc)
通过代码不难看出,装饰器接收一个参数,就是我们要装饰的目标方法。 处理完扩展的内容后,返回一个方法供后续调用,同时失去对原始方法对象的访问。 当我们装饰某个东西的时候,实际上就是改变被装饰方法的入口引用,使其指向装饰器返回的方法的入口点javascript 类的定义,从而实现我们对原有功能的扩展和修改。
ES7 的装饰器
ES7中的装饰器也借用了这种语法糖,但是依赖于ES5的Object.defineProperty方法。
目的。 定义属性
Object.defineProperty() 方法直接在对象上定义新属性,或更改对象的现有属性,并返回该对象。
此方法允许精确添加或修改对象属性。 通过形式参数添加的普通属性会创建在属性枚举期间公开的属性(for...in 或 Object.keys 方法),其值可以更改,也可以删除。 这种方法允许更改默认的额外细节。 默认情况下,使用Object.defineProperty()添加的属性值是不可变的。
语法
<code class="js">Object.defineProperty(obj, prop, descriptor)
在ES6中,由于Symbol类型的特殊性,使用Symbol类型的值作为对象的key与常规的定义或改变有所不同,而Object.defineProperty就是将key定义为Symbol的方式之一财产。
属性描述符
目前对象中存在两种主要类型的属性描述符:数据描述符和访问描述符。
描述符必须是这两种方式之一; 不是同时三个。
数据描述符和访问描述符都有以下可选通配符:
可配置
当且仅当属性的configurable为true时,属性描述符才可以被改变,并且属性也可以从相应的对象中删除。 默认为 false。
可枚举的
enumerable 定义对象的属性是否可以在 for...in 循环和 Object.keys() 中枚举。
当且仅当属性的可枚举性为 true 时,此属性才能出现在对象的枚举属性中。 默认为 false。
数据描述符还具有以下可选通配符:
价值
该属性对应的值。 可以是任何有效的 JavaScript 值(数字、对象、函数等)。 默认值是未定义的。
可写
当且仅当属性的 writable 为 true 时,才可以通过赋值运算符更改该值。 默认为 false。
访问描述符还具有以下可选通配符:
得到
一种为属性提供 getter 的方法,如果没有 getter,则为未定义。 该方法的返回值用作属性值。 默认值是未定义的。
放
一种为属性提供 setter 的方法,如果没有 setter,则为未定义。 此方法将接受唯一参数并将该参数的新值分配给属性。 默认值是未定义的。
如果描述符没有任何 value、writable、get 和 set 关键字,那么它将被视为数据描述符。 如果描述符同时具有(value 或 writable)和(get 或 set)关键字,则会引发异常。使用类装饰
@testable
class MyTestableClass {
// ...
}
function testable(target) {
target.isTestable = true;
}
MyTestableClass.isTestable // true
在上面的代码中,@testable是一个装饰器。 它通过添加静态属性 isTestable 来更改 MyTestableClass 类的行为。 可测试函数的参数目标是 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
上面的代码中javascript 类的定义,装饰器testable可以接受参数,相当于改变了装饰器的行为。
请注意,装饰器会在代码编译时而不是运行时更改类的行为。 这意味着装饰器可以在编译阶段运行代码。 换句话说,装饰器本质上是在编译时执行的函数。
前面的反例是给类添加静态属性。 如果要添加实例属性,可以通过目标类的原型对象来操作。
这是另一个反例。
// 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 的实例中。
装饰方法
装饰器除了可以装饰类之外,还可以装饰类的属性。
class Person {
@readonly
name() { return `${this.first} ${this.last}` }
}
上面的代码中,使用了readonly装饰器来装饰“class”的name方法。
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);
另外,上面的代码表明,装饰器(readonly)会改变属性的描述对象(描述符),然后用改变后的描述对象来定义属性。
功能性装饰方式
由于函数增强,装饰器只能用在类和类方法上,不能用在函数上。
另一方面,如果必须修饰一个函数,可以直接以高阶函数的形式执行。
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);
核心装饰器.js
core-decorators.js 是一个第三方模块,提供了几种常用的装饰器,通过它你可以更好的理解装饰器。
@自动绑定
autobind 装饰器使方法中的 this 对象绑定原始对象。
@只读
只读装饰器使属性或方法不可写。
@覆盖
override装饰器检测泛型类型的方式正确地覆盖了父类的同名方法。 如果不正确,就会报错。
import { override } from 'core-decorators';
class Parent {
speak(first, second) {}
}
class Child extends Parent {
@override
speak() {}
// SyntaxError: Child#speak() does not properly override Parent#speak(first, second)
}
// or
class Child extends Parent {
@override
speaks() {}
// SyntaxError: No descriptor matching Child#speaks() was found on the prototype chain.
//
// Did you mean "speak"?
}
@deprecate(别名@deprecated)
deprecate 或 deprecated 装饰器会向控制台打印一条警告,表明该方法将被弃用。
import { deprecate } from 'core-decorators';
class Person {
@deprecate
facepalm() {}
@deprecate('We stopped facepalming')
facepalmHard() {}
@deprecate('We stopped facepalming', { url: 'http://knowyourmeme.com/memes/facepalm' })
facepalmHarder() {}
}
let person = new Person();
person.facepalm();
// DEPRECATION Person#facepalm: This function will be removed in future versions.
person.facepalmHard();
// DEPRECATION Person#facepalmHard: We stopped facepalming
person.facepalmHarder();
// DEPRECATION Person#facepalmHarder: We stopped facepalming
//
// See http://knowyourmeme.com/memes/facepalm for more details.
//
@suppressWarnings
overrideWarnings 装饰器会抑制已弃用的装饰器进行的 console.warn() 调用。 但是,异步代码进行的调用除外。
使用场景装饰器具有注释的效果
@testable
class Person {
@readonly
@nonenumerable
name() { return `${this.first} ${this.last}` }
}
从里面的代码我们一眼就可以看出,Person类是可测试的,而name方法是只读的,不可枚举的。
React 的连接
实际开发中,React与Redux库结合使用时,往往需要这样写。
class MyReactComponent extends React.Component {}
export default connect(mapStateToProps, mapDispatchToProps)(MyReactComponent);
有了装饰器,就可以重写里面的代码decorate
@connect(mapStateToProps, mapDispatchToProps)
export default class MyReactComponent extends React.Component {}
相对来说,后一种写法似乎更容易理解。
新功能警报或权限
单击菜单时,将执行风暴拦截。 如果菜单有新功能更新,将会显示弹窗。
/**
* @description 在点击时,如果有新功能提醒,则弹窗显示
* @param code 新功能的code
* @returns {function(*, *, *)}
*/
const checkRecommandFunc = (code) => (target, property, descriptor) => {
let desF = descriptor.value;
descriptor.value = function (...args) {
let recommandFuncModalData = SYSTEM.recommandFuncCodeMap[code];
if (recommandFuncModalData && recommandFuncModalData.id) {
setTimeout(() => {
this.props.dispatch({type: 'global/setRecommandFuncModalData', recommandFuncModalData});
}, 1000);
}
desF.apply(this, args);
};
return descriptor;
};
加载中
在React项目中,我们在向后台请求数据时,可能需要页面有一个加载动画。 这时候就可以使用装饰器来优雅地实现功能了。
@autobind
@loadingWrap(true)
async handleSelect(params) {
await this.props.dispatch({
type: 'product_list/setQuerypParams',
querypParams: params
});
}
loadWrap函数如下:
export function loadingWrap(needHide) {
const defaultLoading = (
加载中...
);
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;
};
}
问题:这里你可以想一下,如果我们不希望每次请求数据时都出现loading,而是只在后台请求时间小于300ms时才显示loading,这里怎么改呢?
参考