
Decorators 是ES7中添加的JavaScript新特性。熟悉Typescript的同學應該更早的接觸到這個特性,TypeScript早些時候已經支持Decorators的使用,而且提供了ES5的支持。本文會對Decorators做詳細的講解,相信你會體驗到它給編程帶來便利和優雅。
我在專職做前端開發之前, 是一名專業的.NET程序員,對.NET中的“特性”使用非常熟悉。在類、方法或者屬性上寫上一個中括號,中括號里面初始化一個特性,就會對類,方法或者屬性的行為產生影響。這在AOP編程,以及ORM框架中特別有用,就像魔法一樣。 但是當時JavaScript並沒有這樣的特性。在TypeScript中第一次使用Decorators,是因為我們要對整個應用程序的上下文信息做序列化處理,需要一種簡單的方法,在原來的領域模型上打上一個標簽來標識是否會序列化或者序列化的行為控制,這種場景下Decorators發揮了它的威力。 后來我們需要重構我們的狀態管理,在可變的類定義和不可變對象的應用間進行轉換,如果使用Decorators,不論從編的便利性還是解耦的角度都產生了令人驚喜的效果。 一直想把Decorators的相關使用整理出一個通俗的文檔,使用最簡單的方式來闡述這一話題,一直沒有下筆。無意間在網絡上發現了一篇文章(https://cabbageapps.com/fell-love-js-decorators/) , 這篇文章的行文和我要表達的內容正好相符,於是拿過來做重新編輯和改編。喜歡看英文的同學可以點擊鏈接閱讀原文。

1.0 裝飾器模式
如果我們在搜索引擎中直接搜索“decorators”或者“裝飾器”,和編程相關的結果中,會看到設計模式中的裝飾器模式的介紹。

更直觀的例子如下:

上圖中WeaponAccessory就是一個裝飾器,他們添加額外的方法和熟悉到基類上。如果你看不明白沒關系,跟隨我一步步地實現你自己的裝飾器,自然就會明白了。下面這張圖,可以幫你直觀的理解裝飾器。

我們簡單的理解裝飾器,可以認為它是一種包裝,對對象,方法,熟悉的包裝。當我們需要訪問一個對象的時候,如果我們通過這個對象外圍的包裝去訪問的話,被這個包裝附加的行為就會被觸發。例如 一把加了消聲器的槍。消聲器就是一個裝飾,但是它和原來的槍成為一個整體,開槍的時候消聲器就會發生作用。
從面向對象的角度很好理解這個概念。那么我們如何在JavaScript中使用裝飾器呢?
1.1 開始 Decorators 之旅
Decorators 是ES7才支持的新特性,但是借助Babel 和 TypesScript,我們現在就可以使用它了, 本文以TypesScript為例。
首先修改tsconfig.json文件,設置 experimentalDecorators 和 emitDecoratorMetadata為true。
{ "compilerOptions": { "target": "es2015", "module": "commonjs", "sourceMap": true, "emitDecoratorMetadata": true, "experimentalDecorators": true }, "exclude": [ "node_modules", ] }

我們先從效果入手,然后再層層剖析。先看下面的一段代碼:
function leDecorator(target, propertyKey: string, descriptor: PropertyDescriptor): any { var oldValue = descriptor.value; descriptor.value = function() { console.log(`Calling "${propertyKey}" with`, arguments,target); let value = oldValue.apply(null, [arguments[1], arguments[0]]); console.log(`Function is executed`); return value + "; This is awesome"; }; return descriptor; } class JSMeetup { speaker = "Ruban"; //@leDecorator welcome(arg1, arg2) { console.log(`Arguments Received are ${arg1} ${arg2}`); return `${arg1} ${arg2}`; } } const meetup = new JSMeetup(); console.log(meetup.welcome("World", "Hello"));

運行上面的代碼,得到的結果如下:

下面我們修改代碼,將第17行的注釋放開。

再次運行代碼,結果如下:

注意上圖中左側的輸出結果,和右側顯示的代碼行號。我們現在可以肯定的是,加上了 @leDecorator 標簽之后,函數welcome的行為發生了改變,觸發改變的地方是leDecorator函數。 根據我們上面對裝飾器的基本理解,我們可以認為leDecorator是welcome的裝飾器。
<b>裝飾器和被裝飾者之間通過 @ 符進行連接</b>。
在JavaScript層面我們已經感性的認識了裝飾器,我們的代碼裝飾的是一個函數。在JavaScript中,一共有4類裝飾器:
- Method Decorator 函數裝飾器
- Property Decorators 熟悉裝飾器
- Class Decorator 類裝飾器
- Parameter Decorator 參數裝飾器
下面我們逐一進行攻破!Come on!

1.2 函數裝飾器
第一個要被攻破的裝飾器是函數裝飾器,這一節是本文的核心內容,我們將通過對函數裝飾器的講解來洞察JavaScript Decorators的本質。
通過使用 函數裝飾器,我們可以控制函數的輸入和輸出。
下面是函數裝飾器的定義:
MethodDecorator = <T>(target: Object, key: string, descriptor: TypedPropertyDescriptor<T>) => TypedPropertyDescriptor<T> | Void;
只要遵循上面的定義,我們就可以自定義一個函數裝飾器,三個參數的含義如下:
- target -> 被裝飾的對象
- key -> 被裝飾的函數名
- descriptor -> 被傳遞過來的屬性的屬性描述符. 可以通過 Object.getOwnPropertyDescriptor()方法來查看屬性描述符。
關於屬性描述符更詳細內容 可以參考 https://www.jianshu.com/p/19529527df80。
簡單來講,屬性描述符可以用來配置一個對象的某個屬性的返回值,get/set 行為,是否可以被刪除,是否可以被修改,是否可以被枚舉等特性。為了你能順暢的理解裝飾器,我們下面看一個直觀一點的例子。
打開瀏覽器控制台,輸入如下代碼:
var o, d; var o = { get foo() { return 17; }, bar:17, foobar:function(){return "FooBar"} }; d = Object.getOwnPropertyDescriptor(o, 'foo'); console.log(d); d = Object.getOwnPropertyDescriptor(o, 'bar'); console.log(d); d = Object.getOwnPropertyDescriptor(o, 'foobar'); console.log(d);
結果如下:

這里我們定義了一個對象o,定義了三個屬性——foo,bar和foobar,之后通過Object.getOwnPropertyDescriptor()獲取每個屬性的描述符並打印出來。下面我們對value , enumerable , configurable 和 writable 做簡要的說明。
- value – >字面值或者函數/屬性計算后的返回值。
- enumerable -> 是否可以被枚舉 (是否可以在 (for x in obj)循環中被枚舉出來)
- configurable – >屬性是否可以被配置
- writable -> 屬性是否是可寫的.
每個屬性或者方法都有自己的一個描述符,通過描述符我們可以修改屬性的行為或者返回值。下面關鍵來了:
<b>裝飾器的本質就是修改描述符</b>
是時候動手寫一個裝飾器了。
1.2.1 方法裝飾器實例
下面我們通過方法裝飾器來修改一個函數的輸入和輸出。
function leDecorator(target, propertyKey: string, descriptor: PropertyDescriptor): any { var oldValue = descriptor.value; descriptor.value = function() { console.log(`Calling "${propertyKey}" with`, arguments,target); // Executing the original function interchanging the arguments let value = oldValue.apply(null, [arguments[1], arguments[0]]); //returning a modified value return value + "; This is awesome"; }; return descriptor; } class JSMeetup { speaker = "Ruban"; //@leDecorator welcome(arg1, arg2) { console.log(`Arguments Received are ${arg1}, ${arg2}`); return `${arg1} ${arg2}`; } } const meetup = new JSMeetup(); console.log(meetup.welcome("World", "Hello"));
在不使用裝飾器的時候,輸出值為:
Arguments Received are World, Hello
World Hello
啟用裝飾器后,輸出值為:
Calling "welcome" with { '0': 'World', '1': 'Hello' } JSMeetup {} Arguments Received are Hello, World Hello World; This is awesome
我們看到,方法輸出值發成了變化。現在去看我們定義的方法裝飾器,通過參數,leDecorator在執行時獲取了調用對象的名稱,被裝飾方法的參數,被裝飾方法的描述符。 首先通過oldValue變量保存了方法描述符的原值,即我們定義的welcome方法。接下來對descriptor.value進行了重新賦值。

在新的函數中首先調用了原函數,獲得了返回值,然后修改了返回值。 最后return descriptor,新的descriptor會被應用到welcome方法上,此時整合函數體已經被替換了。
通過使用裝飾器,我們實現了對原函數的包裝,可以修改方法的輸入和輸出,這意味着我們可以應用各種想要的魔法效果到目標方法上。

這里有幾點需要注意的地方:
- 裝飾器在class被聲明的時候被執行,而不是class實例化的時候。
- 方法裝飾器返回一個值
- 存儲原有的描述符並且返回一個新的描述符是我們推薦的做法. 這在多描述符應用的場景下非常有用。
- 設置描述符的value的時候,不要使用箭頭函數。
現在我們完成並理解了第一個方法裝飾器。下面我們來學校屬性裝飾器。
1.3 屬性裝飾器
屬性裝飾器和方法裝飾器很類似,通過屬性裝飾器,我們可以用來重新定義getters、setters,修改enumerable, configurable等屬性。
屬性裝飾器定義如下:
PropertyDecorator = (target: Object, key: string) => void;
參數說明如下:
- target:屬性擁有者
- key:屬性名
在具體使用屬性裝飾器之前,我們先來簡單了解下Object.defineProperty方法。Object.defineProperty方法通常用來動態給一個對象添加或者修改屬性。下面是一段示例:
var o = { get foo() { return 17; }, bar:17, foobar:function(){return "FooBar"} }; Object.defineProperty(o, 'myProperty', { get: function () { return this['myProperty']; }, set: function (val) { this['myProperty'] = val; }, enumerable:true, configurable:true });

在調試控制台測試上面的代碼。

從結果中,我們看到,利用Object.defineProperty,我們動態添給對象添加了屬性。下面我們基於Object.defineProperty來實現一個簡單的屬性裝飾器。
function realName(target, key: string): any { // property value var _val = target[key]; // property getter var getter = function () { return "Ragularuban(" + _val + ")"; }; // property setter var setter = function (newVal) { _val = newVal; }; // Create new property with getter and setter Object.defineProperty(target, key, { get: getter, set: setter }); } class JSMeetup { //@realName public myName = "Ruban"; constructor() { } greet() { return "Hi, I'm " + this.myName; } } const meetup = new JSMeetup(); console.log(meetup.greet()); meetup.myName = "Ragul"; console.log(meetup.greet());

在不適用裝飾器時,輸出結果為:
Hi, I'm Ruban Hi, I'm Ragul
啟用裝飾器之后,結果為:
Hi, I'm Ragularuban(Ruban) Hi, I'm Ragularuban(Ragul)
是不是很簡單呢? 接下來是Class裝飾器。
1.4 Class 裝飾器
Class裝飾器是通過操作Class的構造函數,來實現對Class的相關屬性和方法的動態添加和修改。
下面是Class裝飾器的定義:
ClassDecorator = <TFunction extends Function>(target: TFunction) => TFunction;
ClassDecorator只接收一個參數,就是Class的構造函數。下面的示例代碼,修改了類原有的屬性speaker,並動態添加了一個屬性extra。
function AwesomeMeetup<T extends { new (...args: any[]): {} }>(constructor: T) { return class extends constructor implements extra { speaker: string = "Ragularuban"; extra = "Tadah!"; } } //@AwesomeMeetup class JSMeetup { public speaker = "Ruban"; constructor() { } greet() { return "Hi, I'm " + this.speaker; } } interface extra { extra: string; } const meetup = new JSMeetup() as JSMeetup & extra; console.log(meetup.greet()); console.log(meetup.extra);
在不啟用裝飾器的情況下輸出值為:

在啟用裝飾器的情況下,輸出結果為:

這里需要注意的是,<b>構造函數只會被調用一次</b>。
下面我來學習最后一種裝飾器,參數裝飾器。
1.5 參數裝飾器
如果通過上面講過的裝飾器來推論參數裝飾器的作用,可能會是修改參數,但事實上並非如此。參數裝飾器往往用來對特殊的參數進行標記,然后在方法裝飾器中讀取對應的標記,執行進一步的操作。例如:
function logParameter(target: any, key: string, index: number) { var metadataKey = `myMetaData`; if (Array.isArray(target[metadataKey])) { target[metadataKey].push(index); } else { target[metadataKey] = [index]; } } function logMethod(target, key: string, descriptor: any): any { var originalMethod = descriptor.value; descriptor.value = function (...args: any[]) { var metadataKey = `myMetaData`; var indices = target[metadataKey]; console.log('indices', indices); for (var i = 0; i < args.length; i++) { if (indices.indexOf(i) !== -1) { console.log("Found a marked parameter at index" + i); args[i] = "Abrakadabra"; } } var result = originalMethod.apply(this, args); return result; } return descriptor; } class JSMeetup { //@logMethod public saySomething(something: string, @logParameter somethingElse: string): string { return something + " : " + somethingElse; } } let meetup = new JSMeetup(); console.log(meetup.saySomething("something", "Something Else"));

上面的代碼中,我們定義了一個參數裝飾器,該裝飾器將被裝飾的參數放到一個指定的數組中。在方法裝飾器中,查找被標記的參數,做進一步的處理
不啟用裝飾器的情況下,輸出結果如下:

啟用裝飾器的情況下,輸出結果如下:

1.6 小結
現在我們已經學習了所有裝飾器的使用,下面總結一下關鍵用法:
- 方法裝飾器的核心是 方法描述符
- 屬性裝飾器的核心是 Object.defineProperty
- Class裝飾器的核心是 構造函數
- 參數裝飾器的主要作用是標記,要結合方法裝飾器來使用
下面是參考文章:
https://www.typescriptlang.org/docs/handbook/decorators.html
https://github.com/Microsoft/TypeScript-Handbook/blob/master/pages/Decorators.md
https://survivejs.com/react/appendices/understanding-decorators/
https://medium.com/google-developers/exploring-es7-decorators-76ecb65fb841
https://blog.wolksoftware.com/decorators-metadata-reflection-in-typescript-from-novice-to-expert-part-ii
https://github.com/arolson101/typescript-decorators
更多精彩內容,歡迎關注玄魂工作室微信訂閱號。
