今回はJSデコレーター関数の使い方と、JSデコレーター関数を使う際の注意点を紹介します。実際の事例を見てみましょう。
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)
コードから、デコレータがパラメータを受け取ることは難しくありません。これは、拡張されたコンテンツを処理した後、後で呼び出すためのメソッドを返します。また、元のメソッド オブジェクトへのアクセスも失います。関数に装飾を適用する場合、実際には装飾されたメソッドのエントリ参照を変更して、デコレータによって返されたメソッドのエントリ ポイントを指すようにします。これにより、元の関数を拡張および変更できるようになります。
ES7 デコレータ
ES7 のデコレータもこの糖衣構文を利用しますが、ES5 の Object.defineProperty メソッドに依存します。
Object.defineProperty
Object.defineProperty() メソッドは、オブジェクトの新しいプロパティを直接定義するか、オブジェクトの既存のプロパティを変更して、オブジェクトを返します。
このメソッドを使用すると、オブジェクトのプロパティを正確に追加または変更できます。割り当てによって追加された通常のプロパティは、プロパティの列挙中に公開されるプロパティを作成し (for...in メソッドまたは Object.keys メソッド)、これらの値は変更または削除できます。このアプローチにより、これらの追加の詳細をデフォルト値から変更できるようになります。デフォルトでは、Object.defineProperty() を使用して追加されたプロパティ値は不変です。
構文
Object.defineProperty(obj, prop, descriptor)
obj: プロパティが定義されるオブジェクト。
prop: 定義または変更するプロパティの名前。
descriptor: 定義または変更される属性記述子。
戻り値: 関数に渡されるオブジェクト。
ES6ではSymbol型の特殊性により、Symbol型の値をオブジェクトのキーとして利用することが従来の定義や変更とは異なり、Object.definePropertyはその定義方法の一つとなります。キーが Symbol であるプロパティ。
属性記述子
現在オブジェクトに存在する属性記述子には、データ記述子とアクセス記述子の 2 つの主な形式があります。
データ記述子は、書き込み可能または書き込み不可能な値を持つプロパティです。
アクセス記述子は、getter-setter 関数のペアによって記述されるプロパティです。
記述子は、これら 2 つの形式のいずれかである必要があり、同時に両方を使用することはできません。
データ記述子とアクセス記述子の両方に、次のオプションのキー値があります:
configurable
属性の configurable が true の場合に限り、属性記述子は変更でき、属性記述子は変更できます。プロパティは、対応するオブジェクトから削除することもできます。デフォルトは false です。
enumerable
enumerable は、オブジェクトのプロパティを for...in ループと Object.keys() で列挙できるかどうかを定義します。
プロパティの列挙可能値が true の場合に限り、そのプロパティはオブジェクトの列挙プロパティに表示されます。デフォルトは false です。
データ記述子には、次のオプションのキー値もあります:
value
该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。默认为 undefined。
writable
当且仅当该属性的 writable 为 true 时,value 才能被赋值运算符改变。默认为 false。
存取描述符同时具有以下可选键值:
get
一个给属性提供 getter 的方法,如果没有 getter 则为 undefined。该方法返回值被用作属性值。默认为 undefined。
set
一个给属性提供 setter 的方法,如果没有 setter 则为 undefined。该方法将接受唯一参数,并将该参数的新值分配给该属性。默认为 undefined。
如果一个描述符不具有value,writable,get 和 set 任意一个关键字,那么它将被认为是一个数据描述符。如果一个描述符同时有(value或writable)和(get或set)关键字,将会产生一个异常。
用法
类的装饰
@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 的实例上面。
方法的装饰
装饰器不仅可以装饰类,还可以装饰类的属性。
class Person { @readonly name() { return `${this.first} ${this.last}` } }
上面代码中,装饰器 readonly 用来装饰“类”的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);
装饰器第一个参数是 类的原型对象,上例是 Person.prototype,装饰器的本意是要“装饰”类的实例,但是这个时候实例还没生成,所以只能去装饰原型(这不同于类的装饰,那种情况时target参数指的是类本身);
第二个参数是 所要装饰的属性名
第三个参数是 该属性的描述对象
另外,上面代码说明,装饰器(readonly)会修改属性的 描述对象(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);
core-decorators.js
core-decorators.js是一个第三方模块,提供了几个常见的装饰器,通过它可以更好地理解装饰器。
@autobind
autobind 装饰器使得方法中的this对象,绑定原始对象。
@readonly
readonly 装饰器使得属性或方法不可写。
@override
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
suppressWarnings 装饰器抑制 deprecated 装饰器导致的 console.warn() 调用。但是,异步代码发出的调用除外。
使用场景
装饰器有注释的作用
@testable class Person { @readonly @nonenumerable name() { return `${this.first} ${this.last}` } }
有了装饰器,就可以改写上面的代码。装饰
@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; };
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 = ( <p className="toast-loading"> <Loading className="loading-icon"/> <p>加载中...</p> </p> ); 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,这里需要怎么改?
相信看了本文案例你已经掌握了方法,更多精彩请关注php中文网其它相关文章!
推荐阅读:
以上がJSデコレータ関数の使い方の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。