[TOC] 一. Decorator裝飾器 修飾器是 加入的新特性, 中進行了大量使用,有很多內置的修飾器,後端的同學一般稱之為 “註解” 。修飾器的作用,實際上就是設計模式中常說的 裝飾者模式 的一種實現,早在 開始,設計模式原生化就已經是非常明顯的趨勢了,無論是 和`Iterator Proxy ...
目錄
一. Decorator裝飾器
修飾器是ES7
加入的新特性,Angular
中進行了大量使用,有很多內置的修飾器,後端的同學一般稱之為“註解”。修飾器的作用,實際上就是設計模式中常說的裝飾者模式的一種實現,早在ES6
開始,設計模式原生化就已經是非常明顯的趨勢了,無論是for..of..
和Iterator
介面的配合內化了迭代者模式,Proxy
對象實現的代理模式等等,都可以看出Javascript
逐漸走向標準化的趨勢和決心。
裝飾者模式,是指在不必改變原類文件或使用繼承的情況下,動態地擴展一個對象的功能,為對象增加額外特性的一種設計模式。考慮到javascript
中函數參數為對象時只傳遞地址這一特性,裝飾者模式實際上是非常好復現的,掌握其基本知識對於理解Angular
技術棧的原理和執行流程是必不可少的,從結果的角度來看,使用裝飾器和直接修改類的定義沒有什麼區別,但使用裝飾器更符合開放封閉原則,且更符合聲明式的思想,本文著重分析Typescript
中支持的幾種不同的裝飾器用法。
二. Typescript中的裝飾器
2.1 類裝飾器
類裝飾器,就是用來裝飾類的,它只接受一個參數,就是被裝飾的類。下麵的示例使用@testable
修飾器為已定義的類加上一個__testable
屬性:
//裝飾器修改的是類定義的表現,故在javascript中模擬時需要直接將變化添加至原型上
function testable(target: Function):void{
target.prototype.__testable = false;
}
//使用類裝飾器
@testable
class Person{
constructor(){}
}
//測試裝飾後的結果
let person = new Person();
console.log(person.__testable);//false
另一方面,我們可以使用工廠函數的方法生成一個可接收附加參數的裝飾器,藉助高階函數的思路不難理解,例如Angular
中常見的這種形式:
//Angular中的組件定義
@Component({
selector:'hero-detail',
templateUrl:'hero-detail.html',
styleUrls:['style.css']
})
export Class MyComponent{
constructor(){}
}
//@Component裝飾者類的作用機制可以理解為:
function Component(params:any){
return function(target: Function):void{
target.prototype.metadata = params;
}
}
這樣在組件被實例化時,就可以獲取到傳入的元數據信息。換句話說,Component({...})
執行後返回的函數才是真正的類裝飾器,Component
是一個接受參數然後生成裝飾器的函數,也就是裝飾器工廠,從元編程的角度來講,相當於修改了new
操作符的行為。
2.2 方法裝飾器
方法修飾器聲明在一個方法的聲明之前,會被應用到方法的屬性描述符上,可以用來檢視,修改或者替換方法定義。它接收如下三個參數:
- 1.靜態成員時參數是類的構造函數,實例成員時傳入類的原型對象。
- 2.成員名
- 3.成員屬性描述符
下麵的裝飾器@enumerable
將被修飾對象修改為可枚舉:
//方法裝飾器,返回值會直接賦值給方法的屬性描述符。
function enumerable(target: any, propertyKey: string, descriptor:PropertyDescriptor):void{
descriptor.enumerable = true;
}
class Person{
constructor(){}
@enumerable//使用方法裝飾器
sayHi(){
console.log('Hi');
}
}
//測試裝飾後的結果
let person = new Person();
console.log(person.__testable);//false
更常用的方式依然是利用高階函數返回一個可被外部控制的裝飾器:
function enumerable(value: boolean){
return function (target: any, propertyKey: string, descriptor:PropertyDescriptor):void{
descriptor.enumerable = true;
}
}
2.3 訪問器裝飾器
訪問器,一般指屬性的get/set
方法,和普通方法裝飾器用法一致,需要註意的是typescript中不支持同時裝飾一個成員的get
訪問器和set
訪問器。
2.4 屬性裝飾器
屬性裝飾器表達式運行時接收兩個參數:
- 1.對於靜態成員來說是類的構造函數,對於實例成員來說是類的原型對象。
- 2.成員名
Typescript官方文檔給出的示例是這樣的:
class Greeter {
@format("Hello, %s") greeting: string;
constructor(message: string){
this.greeting = message;
}
greet(){
let formatString = getFormat(this, 'greeting');
return formatString.replace('s%',this.greeting);
}
}
然後定義@format
裝飾器和getFormat
函數:
.import "reflect-metadata";
const formatMetadataKey = Symbol("format");
function format(formatString: string) {
return Reflect.metadata(formatMetadataKey, formatString);
}
function getFormat(target: any, propertyKey: string) {
return Reflect.getMetadata(formatMetadataKey, target, propertyKey);
}
與方法裝飾器相比,屬性裝飾器的形參列表中並沒有屬性描述符,因為目前沒有辦法在定義一個原型對象的成員時描述一個實例屬性,也無法監視屬性的初始化方法。TS中的屬性描述符單獨使用時只能用來監視類中是否聲明瞭某個名字的屬性,示例中通過外部功能擴展了其實用性。Angular中最常見的屬性修飾器就是Input( )
和output( )
。
2.5 參數裝飾器
參數裝飾器一般用於裝飾參數,在類構造函數或方法聲明中裝飾形參。
它在運行時被當做函數調用,傳入下列3個參數:
- 1.靜態成員時接收構造函數,實例成員時接收原型對象。
- 2.成員名
- 3.參數在函數參數列表中的索引。
TS中參數裝飾器單獨使用時只能用來監視一個方法的參數是否被傳入,Typescript官方給出的示例如下:
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
@validate
greet(@required name: string) {//此處使用了參數修飾符
return "Hello " + name + ", " + this.greeting;
}
}
兩個裝飾器的定義如下:
import "reflect-metadata";
const requiredMetadataKey = Symbol('required');
/*
*@required參數裝飾器
*實現的功能就是當函數的參數必須填入時,將相關信息存儲到一個外部的數組中,可以看出參數裝飾器並*未對參數本身做出什麼修改。
*/
function required(target: Object, propertyKey:string | symbol, parameterIndex: number){
let existingRequiredParameters: number[] = Reflect.getOwnMetadata(requiredMetadataKey, target, propertyKey) || [];
existingRequiredParameters.push(parameterIndex);
Reflect.defineMetadata(requiredMetadataKey, existingRequiredParameters, target, propertyKey);
}
/*
*@validate裝飾器為方法裝飾器
*展示瞭如何通過操作方法屬性描述符中的value屬性來實現方法的代理訪問。
*/
function validate(target:any, propertyName: string, descriptor:TypedPropertyDescriptor<Function>){
let method = descriptor.value;//方法的屬性修飾符的value就是方法的函數表達式
descriptor.value = function(){
let requiredParameters: number[] = Reflect.getOwnMetadata(requiredMetadataKey, target, propertyName);//在外部存儲中查找是否有必填參數
if (requiredParameters){
for(let parameterIndex of requiredParameters){
if(parameterIndex >= arguments.length || arguments[parameterIndex] === undefined){
//傳入參數不足或被約束參數為undefined時拋出錯誤。
throw new Error('Missing required argument');
}
}
}
return method.apply(this, arguments);//如果沒有任何錯誤拋出則繼續執行原函數
}
}
在Typescript中,裝飾器的運行順序基本依照參數裝飾器,方法裝飾器,訪問符裝飾器,屬性裝飾器,類裝飾器這樣的順序來運行,所以參數裝飾器和方法裝飾器可以聯合使用實現一些額外功能。
三. 用ES5代碼模擬裝飾器功能
用ES5
來模擬一下上述的方法裝飾器和參數裝飾器聯合作用的例子,就很容易看出裝飾器的作用:
//使用ES5語法模擬裝飾器
function Greeter(message){
this.greeting = message;
}
Greeter.prototype.greet = function(name){
return "Hello " + name + ", " + this.greeting;
}
//外部存儲的必要性校驗
requiredArray = {};
//參數裝飾器
function requireDecorator(FnKey,paramsIndex){
requiredArray[FnKey] = paramsIndex;
}
//裝飾器函數
function validateDecorator(Fn,FnKey){
let method = Fn;
return function(){
let checkParamIndex = requiredArray[FnKey];
if(checkParamIndex > arguments.length-1 || arguments[checkParamIndex] === undefined){
throw new Error('params invalid');
}
return method.apply(this, arguments);
}
}
//運行裝飾
requireDecorator('greet',0);
Greeter.prototype.greet = validateDecorator(Greeter.prototype.greet, 'greet');
//測試裝飾
let greeter = new Greeter('welcome to join the conference');
console.log(greeter.greet('Tony'));
console.log(greeter.greet());
在node環境中運行一下就可以看到,greet( )
方法在未傳入參數時會報錯提示。
四. 小結
裝飾器實際上就是一種更加簡潔的代碼書寫方式,從代碼表現來理解,就是使用閉包和高階函數擴展或者修改了原來的表現,從功能角度來理解,達到了不修改內部實現的前提下動態擴展和修改類定義的目的。