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> ) } }
總結
好啦,今天的分享就要到此結束了哦,希望通過這篇文章大家能夠對裝飾器有一定的瞭解,如有不同意見,歡迎在評論區評論呦~就讓暴風雨來得更猛烈些吧!
參考連結
babel-plugin-transform-decorators-legacy
❉ 作者介紹 ❉

- 淺談低程式碼平臺遠端元件載入方案
- 前端富文字基礎及實現
- 淺談前端埋點&監控
- 如何讓 x == 1 && x == 2 && x == 3 等式成立
- 淺析 path 常用工具函式原始碼
- web components-LitElement實踐
- 模組聯邦淺析
- 效能優化——圖片壓縮、載入和格式選擇
- 如何基於 WebComponents 封裝 UI 元件庫
- CDP 遠端除錯方案
- 如何落地一個智慧機器人
- Form 資料形式配置化設計
- Lerna 執行流程剖析
- Decorator 裝飾器
- 淺析snabbdom中vnode和diff演算法
- 函數語言程式設計(FP)
- 如何利用 SCSS 實現一鍵換膚
- 淺析FormData
- Flutter For Web 編譯的兩種方案
- Web 多執行緒開發利器 Comlink 的剖析與思考