【Angular專(zhuān)題】 (3)裝飾器decorator,一塊語(yǔ)法糖
【Angular專(zhuān)題】 (3)裝飾器decorator,一塊語(yǔ)法糖
一. Decorator裝飾器二. Typescript中的裝飾器2.1 類(lèi)裝飾器2.2 方法裝飾器2.3 訪(fǎng)問(wèn)器裝飾器2.4 屬性裝飾器2.5 參數(shù)裝飾器三. 用ES5代碼模擬裝飾器功能四. 小結(jié)
一. Decorator裝飾器
修飾器是ES7加入的新特性,Angular中進(jìn)行了大量使用,有很多內(nèi)置的修飾器,后端的同學(xué)一般稱(chēng)之為“注解”。修飾器的作用,實(shí)際上就是設(shè)計(jì)模式中常說(shuō)的裝飾者模式的一種實(shí)現(xiàn),早在ES6開(kāi)始,設(shè)計(jì)模式原生化就已經(jīng)是非常明顯的趨勢(shì)了,無(wú)論是for..of..和Iterator接口的配合內(nèi)化了迭代者模式,Proxy對(duì)象實(shí)現(xiàn)的代理模式等等,都可以看出Javascript逐漸走向標(biāo)準(zhǔn)化的趨勢(shì)和決心。
裝飾者模式,是指在不必改變?cè)?lèi)文件或使用繼承的情況下,動(dòng)態(tài)地?cái)U(kuò)展一個(gè)對(duì)象的功能,為對(duì)象增加額外特性的一種設(shè)計(jì)模式。考慮到j(luò)avascript中函數(shù)參數(shù)為對(duì)象時(shí)只傳遞地址這一特性,裝飾者模式實(shí)際上是非常好復(fù)現(xiàn)的,掌握其基本知識(shí)對(duì)于理解Angular技術(shù)棧的原理和執(zhí)行流程是必不可少的,從結(jié)果的角度來(lái)看,使用裝飾器和直接修改類(lèi)的定義沒(méi)有什么區(qū)別,但使用裝飾器更符合開(kāi)放封閉原則,且更符合聲明式的思想,本文著重分析Typescript中支持的幾種不同的裝飾器用法。
二. Typescript中的裝飾器
2.1 類(lèi)裝飾器
類(lèi)裝飾器,就是用來(lái)裝飾類(lèi)的,它只接受一個(gè)參數(shù),就是被裝飾的類(lèi)。下面的示例使用@testable修飾器為已定義的類(lèi)加上一個(gè)__testable屬性:
//裝飾器修改的是類(lèi)定義的表現(xiàn),故在javascript中模擬時(shí)需要直接將變化添加至原型上
function testable(target: Function):void{
target.prototype.__testable = false;
}
//使用類(lèi)裝飾器
@testable
class Person{
constructor(){}
}
//測(cè)試裝飾后的結(jié)果
let person = new Person();
console.log(person.__testable);//false
另一方面,我們可以使用工廠(chǎng)函數(shù)的方法生成一個(gè)可接收附加參數(shù)的裝飾器,借助高階函數(shù)的思路不難理解,例如Angular中常見(jiàn)的這種形式:
//Angular中的組件定義
@Component({
selector:'hero-detail',
templateUrl:'hero-detail.html',
styleUrls:['style.css']
})
export Class MyComponent{
constructor(){}
}
//@Component裝飾者類(lèi)的作用機(jī)制可以理解為:
function Component(params:any){
return function(target: Function):void{
target.prototype.metadata = params;
}
}
這樣在組件被實(shí)例化時(shí),就可以獲取到傳入的元數(shù)據(jù)信息。換句話(huà)說(shuō),Component({...})執(zhí)行后返回的函數(shù)才是真正的類(lèi)裝飾器,Component是一個(gè)接受參數(shù)然后生成裝飾器的函數(shù),也就是裝飾器工廠(chǎng),從元編程的角度來(lái)講,相當(dāng)于修改了new操作符的行為。
2.2 方法裝飾器
方法修飾器聲明在一個(gè)方法的聲明之前,會(huì)被應(yīng)用到方法的屬性描述符上,可以用來(lái)檢視,修改或者替換方法定義。它接收如下三個(gè)參數(shù):
1.靜態(tài)成員時(shí)參數(shù)是類(lèi)的構(gòu)造函數(shù),實(shí)例成員時(shí)傳入類(lèi)的原型對(duì)象。
2.成員名
3.成員屬性描述符
下面的裝飾器@enumerable將被修飾對(duì)象修改為可枚舉:
//方法裝飾器,返回值會(huì)直接賦值給方法的屬性描述符。
function enumerable(target: any, propertyKey: string, descriptor:PropertyDescriptor):void{
descriptor.enumerable = true;
}
class Person{
constructor(){}
@enumerable//使用方法裝飾器
sayHi(){
console.log('Hi');
}
}
//測(cè)試裝飾后的結(jié)果
let person = new Person();
console.log(person.__testable);//false
更常用的方式依然是利用高階函數(shù)返回一個(gè)可被外部控制的裝飾器:
function enumerable(value: boolean){
return function (target: any, propertyKey: string, descriptor:PropertyDescriptor):void{
descriptor.enumerable = true;
}
}
2.3 訪(fǎng)問(wèn)器裝飾器
訪(fǎng)問(wèn)器,一般指屬性的get/set方法,和普通方法裝飾器用法一致,需要注意的是typescript中不支持同時(shí)裝飾一個(gè)成員的get訪(fǎng)問(wèn)器和set訪(fǎng)問(wèn)器。
2.4 屬性裝飾器
屬性裝飾器表達(dá)式運(yùn)行時(shí)接收兩個(gè)參數(shù):
1.對(duì)于靜態(tài)成員來(lái)說(shuō)是類(lèi)的構(gòu)造函數(shù),對(duì)于實(shí)例成員來(lái)說(shuō)是類(lèi)的原型對(duì)象。
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函數(shù):
.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);
}
與方法裝飾器相比,屬性裝飾器的形參列表中并沒(méi)有屬性描述符,因?yàn)槟壳皼](méi)有辦法在定義一個(gè)原型對(duì)象的成員時(shí)描述一個(gè)實(shí)例屬性,也無(wú)法監(jiān)視屬性的初始化方法。TS中的屬性描述符單獨(dú)使用時(shí)只能用來(lái)監(jiān)視類(lèi)中是否聲明了某個(gè)名字的屬性,示例中通過(guò)外部功能擴(kuò)展了其實(shí)用性。Angular中最常見(jiàn)的屬性修飾器就是Input( )和output( )。
2.5 參數(shù)裝飾器
參數(shù)裝飾器一般用于裝飾參數(shù),在類(lèi)構(gòu)造函數(shù)或方法聲明中裝飾形參。
它在運(yùn)行時(shí)被當(dāng)做函數(shù)調(diào)用,傳入下列3個(gè)參數(shù):
1.靜態(tài)成員時(shí)接收構(gòu)造函數(shù),實(shí)例成員時(shí)接收原型對(duì)象。
2.成員名
3.參數(shù)在函數(shù)參數(shù)列表中的索引。
TS中參數(shù)裝飾器單獨(dú)使用時(shí)只能用來(lái)監(jiān)視一個(gè)方法的參數(shù)是否被傳入,Typescript官方給出的示例如下:
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
@validate
greet(@required name: string) {//此處使用了參數(shù)修飾符
return "Hello " + name + ", " + this.greeting;
}
}
兩個(gè)裝飾器的定義如下:
import "reflect-metadata";
const requiredMetadataKey = Symbol('required');
/*
*@required參數(shù)裝飾器
*實(shí)現(xiàn)的功能就是當(dāng)函數(shù)的參數(shù)必須填入時(shí),將相關(guān)信息存儲(chǔ)到一個(gè)外部的數(shù)組中,可以看出參數(shù)裝飾器并*未對(duì)參數(shù)本身做出什么修改。
*/
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裝飾器為方法裝飾器
*展示了如何通過(guò)操作方法屬性描述符中的value屬性來(lái)實(shí)現(xiàn)方法的代理訪(fǎng)問(wèn)。
*/
function validate(target:any, propertyName: string, descriptor:TypedPropertyDescriptor
let method = descriptor.value;//方法的屬性修飾符的value就是方法的函數(shù)表達(dá)式
descriptor.value = function(){
let requiredParameters: number[] = Reflect.getOwnMetadata(requiredMetadataKey, target, propertyName);//在外部存儲(chǔ)中查找是否有必填參數(shù)
if (requiredParameters){
for(let parameterIndex of requiredParameters){
if(parameterIndex >= arguments.length || arguments[parameterIndex] === undefined){
//傳入?yún)?shù)不足或被約束參數(shù)為undefined時(shí)拋出錯(cuò)誤。
throw new Error('Missing required argument');
}
}
}
return method.apply(this, arguments);//如果沒(méi)有任何錯(cuò)誤拋出則繼續(xù)執(zhí)行原函數(shù)
}
}
在Typescript中,裝飾器的運(yùn)行順序基本依照參數(shù)裝飾器,方法裝飾器,訪(fǎng)問(wèn)符裝飾器,屬性裝飾器,類(lèi)裝飾器這樣的順序來(lái)運(yùn)行,所以參數(shù)裝飾器和方法裝飾器可以聯(lián)合使用實(shí)現(xiàn)一些額外功能。
三. 用ES5代碼模擬裝飾器功能
用ES5來(lái)模擬一下上述的方法裝飾器和參數(shù)裝飾器聯(lián)合作用的例子,就很容易看出裝飾器的作用:
//使用ES5語(yǔ)法模擬裝飾器
function Greeter(message){
this.greeting = message;
}
Greeter.prototype.greet = function(name){
return "Hello " + name + ", " + this.greeting;
}
//外部存儲(chǔ)的必要性校驗(yàn)
requiredArray = {};
//參數(shù)裝飾器
function requireDecorator(FnKey,paramsIndex){
requiredArray[FnKey] = paramsIndex;
}
//裝飾器函數(shù)
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);
}
}
//運(yùn)行裝飾
requireDecorator('greet',0);
Greeter.prototype.greet = validateDecorator(Greeter.prototype.greet, 'greet');
//測(cè)試裝飾
let greeter = new Greeter('welcome to join the conference');
console.log(greeter.greet('Tony'));
console.log(greeter.greet());
在node環(huán)境中運(yùn)行一下就可以看到,greet( )方法在未傳入?yún)?shù)時(shí)會(huì)報(bào)錯(cuò)提示。
四. 小結(jié)
裝飾器實(shí)際上就是一種更加簡(jiǎn)潔的代碼書(shū)寫(xiě)方式,從代碼表現(xiàn)來(lái)理解,就是使用閉包和高階函數(shù)擴(kuò)展或者修改了原來(lái)的表現(xiàn),從功能角度來(lái)理解,達(dá)到了不修改內(nèi)部實(shí)現(xiàn)的前提下動(dòng)態(tài)擴(kuò)展和修改類(lèi)定義的目的。
Elasticsearch Angular
版權(quán)聲明:本文內(nèi)容由網(wǎng)絡(luò)用戶(hù)投稿,版權(quán)歸原作者所有,本站不擁有其著作權(quán),亦不承擔(dān)相應(yīng)法律責(zé)任。如果您發(fā)現(xiàn)本站中有涉嫌抄襲或描述失實(shí)的內(nèi)容,請(qǐng)聯(lián)系我們jiasou666@gmail.com 處理,核實(shí)后本網(wǎng)站將在24小時(shí)內(nèi)刪除侵權(quán)內(nèi)容。
版權(quán)聲明:本文內(nèi)容由網(wǎng)絡(luò)用戶(hù)投稿,版權(quán)歸原作者所有,本站不擁有其著作權(quán),亦不承擔(dān)相應(yīng)法律責(zé)任。如果您發(fā)現(xiàn)本站中有涉嫌抄襲或描述失實(shí)的內(nèi)容,請(qǐng)聯(lián)系我們jiasou666@gmail.com 處理,核實(shí)后本網(wǎng)站將在24小時(shí)內(nèi)刪除侵權(quán)內(nèi)容。