Decorator 裝飾器
本文首發於政採雲前端團隊博客:Decorator 裝飾器
https://www.zoo.team/article/decorator
前言
大家在前端開發過程中有遇到過 @ + 方法名
這種寫法嗎?當我第一次看到的時候,直接懵了,這是什麼東東……
遇到困難解決困難,在我的一番查找後,我知道了,原來這東西叫裝飾器,英文名叫 Decorator
,那它到底是幹什麼的呢?接下來就讓我跟大家說道說道~
什麼是裝飾器
裝飾者模式
裝飾者模式就是能夠在不改變對象自身的基礎上,在程序運行期間給對象動態地添加職責。打個比方,一個人在天氣冷的時候要穿棉衣,天氣熱的時候穿短袖,可無論穿什麼,本質上他還是一個人,只不過身上穿了不同的衣服。
所以簡單來說, Decorator
就是一種動態地往一個類中添加新的行爲的設計模式, 它可以在類運行時, 擴展一個類的功能, 並且去修改類本身的屬性和方法, 使其可以在不同類之間更靈活的共用一些屬性和方法。
@
是針對這種設計模式的一個語法糖,不過目前還處於第 2 階段提案中,使用它之前需要使用 Babel 模塊編譯成 ES5 或 ES6。
怎麼使用裝飾器
三方庫使用
Babel 版本 ≥ 7.x
如果項目的 Babel 版本大於等於 7.x,那麼可以使用 @babel/plugin-proposal-decorators
-
安裝
npm install --save-dev @babel/plugin-proposal-decorators
-
配置 .babelrc
{ "plugins": [ ["@babel/plugin-proposal-decorators", { "legacy": true }], ] }
Babel 版本 ≤ 6.x
如果小於等於 6.x,則可以使用 babel-plugin-transform-decorators-legacy
-
安裝
npm install --save-dev @babel/plugin-proposal-decorators
-
配置 .babelrc
{ "plugins": ["transform-decorators-legacy"] }
使用方法
裝飾器的寫法是 @ + 返回裝飾器函數的表達式
,所以其使用方法如下:
@classDecorator
class TargetClass { // 類
@fieldDecorator
targetField = 0; // 類實例屬性
@funDecorator
targetFun() { } // 類方法
@accessorDecorator
get targetGetFun() { } // 類訪問器
}
如果一個對象使用多個裝飾器,那麼執行順序是什麼呢?
function decorator1() {
console.log('decorator1');
return function decFn1(targetClass) {
console.log('decFn1');
return targetClass;
};
}
function decorator2() {
console.log('decorator2');
return function decFn2(targetClass) {
console.log('decFn2');
return targetClass;
};
}
執行順序:
打印結果:
根據以上,我們可知,裝飾器的執行順序爲由外向內進入,由內向外執行。
使用範圍
根據使用方法,我們可以看出裝飾器可以應用於以下幾種類型:
-
類(class)
-
類實例屬性(公共、私有和靜態)
-
類方法(公共、私有和靜態)
-
類訪問器(公共、私有和靜態)
函數的裝飾
當我們看完裝飾器的使用方法和使用範圍時,我們發現,裝飾器不能修飾函數,那原因到底是什麼呢?原因就是函數有函數提升。
var num = 0;
function add () {
num ++;
}
@add
function fn() {}
在這個例子中,我們想要在執行後讓 num 等於 1,但其實結果並不是這樣,因爲函數提升,實際上代碼是這樣執行的:
function add () {
num ++;
}
@add
function fn() {}
var num;
num = 0;
如果一定要裝飾函數的話,可以採用高階函數的形式,這篇文章主要講裝飾器,有關高階函數就不在此贅述了,不瞭解的小夥伴們可自行查閱資料哈~
裝飾器原理
根據裝飾器的使用範圍,可以把它分爲兩大類:類的裝飾與類方法的裝飾,下面就讓我爲大家逐個分享一下。
類的裝飾
傳參
首先我們先根據一個小例子看一下裝飾器接收參數的情況:
function decorator(...args) {
args.forEach((arg, index) => {
console.log(`參數${index}`, arg);
});
}
@decorator
class TargetClass { }
console.log('targetClass:', TargetClass);
打印結果如下:
看到結果,我們發現裝飾器只接收一個參數,就是被裝飾的類定義本身。
返回值
我們繼續通過一個小例子來看返回值的情況:
function returnStr(targetClass) {
return 'hello world~';
}
function returnClass(targetClass) {
return targetClass;
}
@returnStr
class ClassA { }
@returnClass
class ClassB { }
console.log('ClassA:', ClassA);
console.log('ClassB:', ClassB);
結果如下:
根據結果,我們發現裝飾器返回什麼輸出的就是什麼。
結論
通過以上的兩個例子,我們可以得出以下這個結論:
@decorator
class TargetClass { }
// 等同於
class TargetClass { }
TargetClass = decorator(TargetClass) || TargetClass;
所以說,裝飾器的第一個參數就是要裝飾的類,它的功能就是對類進行處理。
類裝飾器的使用
-
添加屬性
因爲裝飾器接收的參數就是類定義本身,所以我們可以給類添加屬性:
function addAttribute(targetClass) { targetClass.isUseDecorator = true; } @addAttribute class TargetClass { } console.log(TargetClass.isUseDecorator); // true
在這個例子中,我們定義了
addAttribute
的裝飾器,用於對TargetClass
添加isUseDecorator
標記,這個用法就跟 Java 中的註解比較相似,僅僅是對目標類型打上一些標記。 -
返回裝飾器函數的表達式
上面有說裝飾器的寫法是
@ + 返回裝飾器函數的表達式
,也就是說,@
後邊可以不是一個方法名,還可以是能返回裝飾器函數的表達式:function addAttribute(content) { return function decFn(targetClass) { targetClass.content = content; return targetClass; }; } @addAttribute('這是內容~~~') class TargetClass { } console.log(TargetClass.content); // 這是內容~~~
我們看到
TargetClass
通過addAttribute
的裝飾,添加了content
這個屬性,並且可以向addAttribute
傳參來給content
屬性賦值,這種使用方法使裝飾器變得更加靈活。 -
添加原型方法
在前面的例子中我們添加的都是類的靜態屬性,但是既然裝飾器接收的參數就是類定義本身,那麼它也可以通過訪問類的
prototype
屬性來添加或修改原型方法:function decorator(targetClass) { targetClass.prototype.decFun = function () { console.log('這裏是裝飾器 decorator 添加的原型方法 decFun~'); }; } @decorator class TargetClass { } const targetClass = new TargetClass(); console.log(targetClass); targetClass.decFun();
結果如下:
以上就是類裝飾器的使用,由此我們可以得出,裝飾器還可以對類型進行靜態標記和方法擴展,還挺有用的對吧~那麼看到這裏,小夥伴們是不是發現了在實際項目中就有類裝飾器的使用,比如 react-redux 的 connect 就是一個類裝飾器、Antd 中的 Form.create 也是一個類裝飾器。
// connect
class App extends React.Component {}
export default connect(mapStateToProps, mapDispatchToProps)(App);
// 等同於
@connect(mapStateToProps, mapDispatchToProps)
export default class App extends React.Component {}
// Form.create
const WrappedApp = Form.create()(App);
// 等同於
@Form.create()
class App extends React.Component {}
類方法的裝飾
傳參
我們把類實例屬性、類方法、類訪問器都歸到這一類中的原因其實是因爲它們三個就是作爲某個對象的屬性 (實例屬性、原型方法、實例訪問器屬性),也就是說它們接收的參數是類似的:
function decorator(...args) {
args.forEach((arg, index) => {
console.log(`參數${index}`, arg);
});
console.log('****************');
}
class TargetClass {
@decorator
field = 0;
@decorator
fn() { }
@decorator
get getFn() { }
}
const targetOne = new TargetClass();
console.log(targetOne.field, Object.getOwnPropertyDescriptor(targetOne, 'field'));
結果如下:
根據結果我們發現,類方法裝飾器接收了三個參數:類定義對象、實例屬性 / 方法 / 實例訪問器屬性名、屬性操作符。眼熟吧,沒錯,它與 Object.defineProperty()
接收的參數很像。
Object.defineProperty(obj, props, descriptor)
Object.defineProperty()
的作用就是直接在一個對象上定義一個新屬性,或者修改一個對象的現有屬性,並返回此對象。該方法一共接收三個參數:
-
要定義屬性的對象(obj)
-
要定義或修改的屬性名或
Symbol
(props) -
要定義或修改的屬性描述符(descriptor)
而對象裏目前存在的屬性描述符有兩種主要形式:數據描述符_和_存取描述符。_數據描述符_是一個具有值的屬性,該值可以是可寫的,也可以是不可寫的;_存取描述符_是由 getter 函數和 setter 函數所描述的屬性。一個描述符只能是這兩者其中之一,不能同時是兩者。
它們共享以下可選鍵值:
-
configurable
屬性是否可以被刪除和重新定義特性,默認值爲
false
-
enumerable
是否會出現在對象的枚舉屬性中,默認值爲
false
數據描述符特有鍵值:
-
value
該屬性對應的值,默認值爲
undefined
-
writable
是否可以被更改,默認值爲
false
存取操作符特有鍵值:
-
get
屬性的
getter
函數,如果沒有getter
,則爲undefined
;默認爲undefined
-
set
屬性的
setter
函數,如果沒有setter
,則爲undefined
;默認爲undefined
講完 Object.defineProperty()
,接下來就讓我們看看該怎麼使用它吧~
類方法裝飾器的使用
讓我們通過一個例子來了解一下:
function readonly(target, name, descriptor) {
descriptor.writable = false;
return descriptor;
}
class Person {
@readonly
name = 'zhangsan';
}
const person = new Person();
console.log(person.name, Object.getOwnPropertyDescriptor(person, 'name'));
打印結果如下:
上面代碼說明,裝飾器會修改屬性的描述對象,然後被修改的描述對象再用來定義屬性。
結論
由此我們可以得出結論:
function changeName(target, name, descriptor) {
descriptor.value = 'lisi';
return descriptor;
}
class Person {
@changeName
name = 'zhangsan';
}
const person = new Person();
// 等同於
class Person {
name = 'zhangsan';
}
const person = new Person();
Object.defineProperty(person, 'name', {
value: 'lisi',
});
裝飾器的應用
在項目中,可能會遇到這樣一種情況,好幾個組件的數據都是調用同一個後端接口獲得,只是傳參不同,有些小夥伴們在寫代碼的時候可能就是每個組件都去手動調用一次後端接口(以 React 項目爲例):
...
export default class CompOne extends Component {
...
getData = async () => { // 調用後端接口
const data = await request('/xxx', {
params: {
id: '123', // 不同組件傳參不同
},
});
this.setState({ data });
}
render() {
...
return (
<div>
...
我是組件一: {data}
...
</div>
)
}
}
遇到這種情況,我們就可以用裝飾器解決呀~
// 裝飾器
function getData(params) {
return (Comp) => {
class WrapperComponent extends Component {
...
getData = async () => {
const data = await request('/xxx', {
params,
});
this.setState({ data });
}
render() {
...
return (
<Comp data={data} />
)
}
}
return Comp;
}
}
// 組件
...
@getData({
id: '123'
})
export default class index extends Component {
...
render() {
...
const data = this.props.data; // 直接從 this.props 中獲取想要的數據
return (
<div>
...
我是組件一: {data}
...
</div>
)
}
}
總結
好啦,今天的分享就要到此結束了哦,希望通過這篇文章大家能夠對裝飾器有一定的瞭解,如有不同意見,歡迎在評論區評論呦~就讓暴風雨來得更猛烈些吧!
參考鏈接
裝飾器(https://www.bookstack.cn/read/es6-3rd/docs-decorator.md)
ES7 提案: Decorators 裝飾器(https://blog.csdn.net/weixin_44691608/article/details/117180409)
Object.defineProperty()(https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty)
babel-plugin-transform-decorators-legacy(https://www.npmjs.com/package/babel-plugin-transform-decorators-legacy)
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/DexxNuRG-x29dZrWCcJDuQ