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的裝飾器。 裝飾器和被裝飾者之間通過 @ 符進行連線。
在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 -> 屬性是否是可寫的.
每個屬性或者方法都有自己的一個描述符,通過描述符我們可以修改屬性的行為或者返回值。下面關鍵來了:
裝飾器的本質就是修改描述符
是時候動手寫一個裝飾器了。
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);
複製程式碼
在不啟用裝飾器的情況下輸出值為:
在啟用裝飾器的情況下,輸出結果為:
這裡需要注意的是,建構函式只會被呼叫一次。
下面我來學習最後一種裝飾器,引數裝飾器。
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裝飾器的核心是 建構函式
- 引數裝飾器的主要作用是標記,要結合方法裝飾器來使用
更多前端好文,關注微信訂閱號“玄魂工作室”,回覆“qd” 即可
下面是參考文章: https://www.typescriptlang.org/docs/handbook/decorators.htmlhttps://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