在 ES6 中增長了對類對象的相關定義和操做(好比 class 和 extends ),這就使得咱們在多個不一樣類之間共享或者擴展一些方法或者行爲的時候,變得並非那麼優雅。這個時候,咱們就須要一種更優雅的方法來幫助咱們完成這些事情。前端
什麼是裝飾器 Python 的裝飾器 在面向對象(OOP)的設計模式中,decorator被稱爲裝飾模式。OOP的裝飾模式須要經過繼承和組合來實現,而Python除了能支持 OOP 的 decorator 外,直接從語法層次支持 decorator。python
若是你熟悉 python 的話,對它必定不會陌生。那麼咱們先來看一下 python 裏的裝飾器是什麼樣子的吧:web
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)
複製代碼
經過代碼咱們也不難看出,裝飾器 decorator 接收一個參數,也就是咱們被裝飾的目標方法,處理完擴展的內容之後再返回一個方法,供之後調用,同時也失去了對原方法對象的訪問。當咱們對某個應用了裝飾之後,其實就改變了被裝飾方法的入口引用,使其從新指向了裝飾器返回的方法的入口點,從而來實現咱們對原函數的擴展、修改等操做。設計模式
ES7 的裝飾器 ES7 中的 decorator 一樣借鑑了這個語法糖,不過依賴於 ES5 的 Object.defineProperty 方法 。bash
Object.defineProperty Object.defineProperty() 方法會直接在一個對象上定義一個新屬性,或者修改一個對象的現有屬性, 並返回這個對象。app
該方法容許精確添加或修改對象的屬性。經過賦值來添加的普通屬性會建立在屬性枚舉期間顯示的屬性(for...in 或 Object.keys 方法), 這些值能夠被改變,也能夠被刪除。這種方法容許這些額外的細節從默認值改變。默認狀況下,使用 Object.defineProperty() 添加的屬性值是不可變的。異步
語法async
Object.defineProperty(obj, prop, descriptor)
複製代碼
obj:要在其上定義屬性的對象。 prop:要定義或修改的屬性的名稱。 descriptor:將被定義或修改的屬性描述符。 返回值:被傳遞給函數的對象。ide
在ES6中,因爲 Symbol類型 的特殊性,用 Symbol類型 的值來作對象的key與常規的定義或修改不一樣,而Object.defineProperty 是定義 key爲 Symbol 的屬性的方法之一。
屬性描述符 對象裏目前存在的屬性描述符有兩種主要形式:數據描述符和存取描述符。
數據描述符是一個具備值的屬性,該值多是可寫的,也可能不是可寫的。 存取描述符是由 getter-setter 函數對描述的屬性。 描述符必須是這兩種形式之一;不能同時是二者。
數據描述符和存取描述符均具備如下可選鍵值:
configurable
當且僅當該屬性的 configurable 爲 true 時,該屬性描述符纔可以被改變,同時該屬性也能從對應的對象上被刪除。默認爲 false。
enumerable
enumerable定義了對象的屬性是否能夠在 for...in 循環和 Object.keys() 中被枚舉。
當且僅當該屬性的 enumerable 爲 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}` }
}
複製代碼
從上面代碼中,咱們一眼就能看出,Person類是可測試的,而name方法是隻讀和不可枚舉的。
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 {}
複製代碼
相對來講,後一種寫法看上去更容易理解。
新功能提醒或權限 菜單點擊時,進行事件攔截,若該菜單有新功能更新,則彈窗顯示。
/**
* @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 = (
<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; }; } 複製代碼
文章就到這裏了
這裏推薦一下個人前端學習交流羣:784783012,裏面都是學習前端的,若是你想製做酷炫的網頁,想學習編程。本身整理了一份2018最全面前端學習資料,從最基礎的HTML+CSS+JS【炫酷特效,遊戲,插件封裝,設計模式】到移動端HTML5的項目實戰的學習資料都有整理,送給每一位前端小夥伴,有想學習web前端的,或是轉行,或是大學生,還有工做中想提高本身能力的,正在學習的小夥伴歡迎加入學習。