不久前,我開發了一個react
應用,使用mobx
作狀態管理。這是一個時而興奮時而困惑,但整體而言很享受的經歷,很快我將會把它寫出來。在使用mobx
開發時,我發現了一個很是有趣的獨特之處,那就是它使用裝飾器來註釋類的屬性。我以前在寫javascript
時還沒用過它,但自從我使用了mobx
提供的這個功能以及作了一些開發後,我發現這是一個有巨大潛力的功能。javascript
裝飾器如今還不是javascript
的核心特性,他們正經過ECMATC39的標準化流程進行工做。不過並不表明咱們不能去熟悉它。
在不久的未來,它將獲得瀏覽器和node
的原生支持,與此同時,babel
也獲得支持。html
Decorator
是decorator function/methored
的縮寫。它是一個函數,它會經過返回一個新函數來修改傳入的函數或方法的行爲。java
你能夠在函數式編程的任何語言中實現裝飾器,好比javascript
,你能夠把函數綁定到一個變量上,也能夠把函數當成函數的參數傳遞。這些語言中的幾種有特殊的語法糖,用來定義和使用裝飾器,其中一個就是python
:node
def cashify(fn): def wrap(): print("$$$$") fn() print("$$$$") return wrap @cashify def sayHello(): print("hello!") sayHello() # $$$$ # hello! # $$$$
讓咱們看看發生了什麼,cashify
函數是一個裝飾器,他接受一個函數做爲參數,它的返回值也是函數。咱們使用python
的pie syntax
把裝飾器應用到sayHello
函數上,本質上和咱們在sayHello
的定義下執行此操做是同樣的:python
def sayHello(): print("hello!") sayHello = cashify(sayHello)
不管咱們裝飾的函數打印什麼,最後的結果都會在他們先後打印$符號。react
爲何我要使用python
的例子來介紹ECMAScript
的裝飾器,很高興你問這個問題!git
python
是一個很好地方式去解釋基礎知識,由於它的裝飾器的概念比它在JS中的工做方式更簡單直接js
和TS
都是用python
的pie syntax
把裝飾器應用到類的函數和屬性上,因此它們外觀和語法格式都很類似好了,那麼js
裝飾器有什麼不一樣呢?es6
python
把傳入的須要裝飾的任何函數當作參數,但由於對象在js
中的特殊工做方式,js裝飾器能夠獲取到更多信息。github
對象在js
中有屬性,而且這些屬性有如下值:編程
const oatmeal = { viscosity: 20, flavor: 'Brown Sugar Cinnamon', };
但除了它的值,每一個屬性還有一些其餘隱藏的信息,用於定義它工做方式的不一樣方面,叫作屬性描述符:
console.log(Object.getOwnPropertyDescriptor(oatmeal, 'viscosity')); /* { configurable: true, enumerable: true, value: 20, writable: true } */
JS在追蹤與這個屬性有關的不少東西:
configurable
決定該屬性的類型可否被修改,以及它可否從對象中刪除enumerable
控制當你在枚舉對象屬性時,該屬性是否顯示(好比當你調用Object.keys(oatmeal)
或者使用for
循環時)writable
控制你是否能夠經過賦值操做符=
修改該屬性的值value
是你訪問這個屬性時,所看到的靜態值。一般,這是你常常看到和關心的屬性描述符的惟一部分。它能夠是任何JS
值,包括一個函數,這會使這個屬性成爲其所屬對象的方法。屬性描述符也有兩個其餘的屬性,爲訪問器描述符(一般稱爲getter
和setter
):
get
是一個返回屬性值而不是用靜態value
屬性的的函數set
是一個特殊的函數,當你給這個屬性賦值時,該函數會將你在等號右邊放置的任何內容做爲參數js
從es5
就已經有了操做屬性描述符的API
,經過Object.getOwnPropertyDescriptor
和Object.defineProperty
的形式。好比我喜歡個人燕麥片的濃度,我可使用這個API
像下邊這樣把它變成只讀的:
Object.defineProperty(oatmeal, 'viscosity', { writable: false, value: 20, }); // 當我試圖設置oatmeal.viscosity爲不一樣的值時,它將會默默地報錯 oatmeal.viscosity = 30; console.log(oatmeal.viscosity); // => 20
我甚至能夠寫一個通用的decorate
函數,能夠修改任何對象的任何屬性的修飾符
function decorate(obj, property, callback) { var descriptor = Object.getOwnPropertyDescriptor(obj, property); Object.defineProperty(obj, property, callback(descriptor)); } decorate(oatmeal, 'viscosity', function(desc) { desc.configurable = false; desc.writable = false; desc.value = 20; return desc; });
第一個主要的裝飾器的提案只與ES
的類有關,而非普通對象。讓咱們設計一些類來表明咱們的粥:
class Porridge { constructor(viscosity = 10) { this.viscosity = viscosity; } stir() { if (this.viscosity > 15) { console.log('This is pretty thick stuff.'); } else { console.log('Spoon goes round and round.'); } } } class Oatmeal extends Porridge { viscosity = 20; constructor(flavor) { super(); this.flavor = flavor; } }
咱們使用一個類來表明咱們的燕麥粥,他繼承自一個更通用的的 Porridge
類。Oatmeal
設置了默認的濃度來覆蓋Porridge
的默認值,而且添加了新的口味屬性。咱們也使用了另外一個es
提案 class fields去覆蓋濃度屬性。
咱們能夠從新建立咱們原始的燕麥粥了:
const oatmeal = new Oatmeal('Brown Sugar Cinnamon'); /* Oatmeal { flavor: 'Brown Sugar Cinnamon', viscosity: 20 } */
很好,咱們獲得了咱們的es6
燕麥粥,咱們要準備寫裝飾器了!
js
裝飾器函數被傳入三個參數:
target
是咱們對象所繼承的類key
是咱們應用裝飾器的屬性的名稱,爲字符串。descriptor
是屬性描述符對象咱們在裝飾器內作什麼依賴於咱們裝飾器的目的。爲了裝飾對象的方法和屬性,咱們須要返回一個新的屬性描述器。咱們能夠經過如下方式寫一個裝飾器來使一個屬性爲只讀:
function readOnly(target, key, descriptor) { return { ...descriptor, writable: false, }; }
咱們能夠像這樣修改咱們的oatmeal類:
class Oatmeal extends Porridge { @readOnly viscosity = 20; // 你也能夠吧@readonly放在屬性上一行 constructor(flavor) { super(); this.flavor = flavor; } }
如今咱們燕麥粥像膠水同樣的濃度不會被幹預了,謝天謝地。
若是咱們想作一些真正有用的東西呢?我在最近的項目時遇到了一種狀況,其中裝飾器節省了我不少開發和維護的開銷。
在我開頭提到的Mobx/React app
中,我有一些不一樣的類做爲數據中心。他們各自都表明與用戶交互的不一樣類別的集合,而且與不一樣的API
端點對話以獲取服務端的數據。爲了處理API
錯誤,我使每一個數據中心在與網絡通訊時都準守一個協議:
ui
中心的networkStatus
屬性爲loading
api
請求處理結果
ui
中心的apiError
屬性爲接收到的錯誤ui
中心的networkStatus
屬性爲idle
我發如今我注意到以前,已經重複了不少次這種模式:
class WidgetStore { async getWidget(id) { this.setNetworkStatus('loading'); try { const { widget } = await api.getWidget(id); // Do something with the response to update local state: this.addWidget(widget); } catch (err) { this.setApiError(err); } finally { this.setNetworkStatus('idle'); } } }
這是不少錯誤處理的樣板。由於我已經在全部更新可觀察屬性的方法上使用了MobX
的@action
裝飾器了(爲了簡單起見,此處未顯示),因此也能夠再添加一個裝飾器用來節省我錯誤處理的代碼。我想出了這個:
function apiRequest(target, key, descriptor) { const apiAction = async function(...args) { // More about this line shortly: const original = descriptor.value || descriptor.initializer.call(this); this.setNetworkStatus('loading'); try { const result = await original(...args); return result; } catch (e) { this.setApiError(e); } finally { this.setNetworkStatus('idle'); } }; return { ...descriptor, value: apiAction, initializer: undefined, }; }
而後我就能夠像這樣替換那些寫在每一個API
操做方法上的模板:
class WidgetStore { @apiRequest async getWidget(id) { const { widget } = await api.getWidget(id); this.addWidget(widget); return widget; } }
個人錯誤處理代碼依然在那,可是我只須要寫一次,而且確保每一個使用它的class
都有setNetworkStatus
和setApiError
方法便可。
我選擇descriptor.value
和調用descriptor.initializer
其中之一的那一行發生了什麼?這是與babel相關的事。個人預感是,這種方式在js
原生支持裝飾器的時候不會起做用,但當考慮到babel處理做爲類屬性的箭頭函數的方式時,就會頗有必要。
當你定義一個類屬性,而且給它賦值一個箭頭函數時,babel
會巧妙地把函數綁定到類正確的實例上而且提供你正確的this
值。經過設置descriptor.initializer
爲一個函數,它會返回你寫的那個函數,而且在其做用域內爲正確的this
值。
一個例子會讓事情變簡單:
class Example { @myDecorator someMethod() { // 在這個例子中,咱們的方法能夠由descriptor.value引用到 } @myDecorator boundMethod = () => { // 在這裏,descriptor.initializer是一個函數,他會返回咱們的boundMethod函數,而且this執行已經被調整爲Example的實例 }; }
除了屬性和方法,你還能夠裝飾整個類。想要裝飾類,你只須要傳入裝飾器函數的第一個參數target
。好比,我想寫一個自動把類註冊爲自定義html
標籤的裝飾器,我在這裏使用了一個閉包,來保證裝飾器可以接收咱們想要爲標籤提供參數的任何名稱:
function customElement(name) { return function(target) { // customElements是一個全局API,用來建立自定義標籤 customElements.define(name, target); }; }
咱們將這樣使用它:
@customElement('intro-message'); class IntroMessage extends HTMLElement { constructor() { super(); const shadow = this.attachShadow({ mode: 'open' }); this.wrapper = this.createElement('div', 'intro-message'); this.header = this.createElement('h1', 'intro-message__title'); this.content = this.createElement('div', 'intro-message__text'); this.header.textContent = this.getAttribute('header'); this.content.innerHTML = this.innerHTML; shadow.appendChild(this.wrapper); this.wrapper.appendChild(this.header); this.wrapper.appendChild(this.content); } createElement(tag, className) { const elem = document.createElement(tag); elem.classList.add(className); return elem; } }
把它加入到咱們的html中,能夠這樣使用它:
<intro-message header="Welcome to Decorators"> <p>Something something content...</p> </intro-message>
瀏覽器中顯示以下:
現在在你的項目中使用裝飾器須要一些轉譯配置。我所見的最直接的教程就在MobX
的文檔中,它有TS
和兩個主要版本的babel
信息。
請記住裝飾器當前仍是發展中的提議,若是你在生產代碼中使用它,你可能須要作一些更新或者持續使用babel
裝飾器插件,直到它成爲ECMA
官方的正式規範。甚至babel也沒有很好地支持,最新版的裝飾器提案包含很大的改動,並無很好地向後兼容上一個版本。
裝飾器像不少最新的js特性同樣,是你工具箱中頗有用的工具,他很大程度的簡化了不一樣和不相關的類的行爲共享。然而過早的採用總須要一些成本。因此使用裝飾器,也須要了解它對你代碼庫的影響。