TypeScript(八)裝飾器
目錄
元數(shù)據(jù)函數(shù)實(shí)現(xiàn)
前言
本文收錄于TypeScript知識(shí)總結(jié)系列文章,歡迎指正!
程序遵循開(kāi)放封閉原則,即在設(shè)計(jì)和編寫軟件時(shí)應(yīng)該盡量避免對(duì)原有代碼進(jìn)行修改,而是通過(guò)添加新的代碼來(lái)擴(kuò)展軟件的功能。
在日常開(kāi)發(fā)中不知你有沒(méi)有遇到以下情況,我們封裝了一個(gè)Request模塊,現(xiàn)在需要對(duì)請(qǐng)求進(jìn)行攔截,訪問(wèn)請(qǐng)求參數(shù),此時(shí)我們可以通過(guò)裝飾器針對(duì)請(qǐng)求函數(shù)或者請(qǐng)求類進(jìn)行訪問(wèn),獲取參數(shù)并解析
定義
在TS中,裝飾器是一種特殊類型的聲明。可以附加到類、方法、屬性或參數(shù)上用于修改類的行為或?qū)傩浴?/strong>
在面向?qū)ο缶幊讨校袝r(shí)需要對(duì)類的行為和功能做出修改,直接修改類的內(nèi)部可能會(huì)使成本升高,或出現(xiàn)其他問(wèn)題;此時(shí)可以使用裝飾器來(lái)修改類,在保證類內(nèi)部結(jié)構(gòu)與功能不變的前提下對(duì)數(shù)據(jù)或行為進(jìn)行迭代
TS中裝飾器可以分為類裝飾器、方法裝飾器、屬性裝飾器和參數(shù)裝飾器。
tips:使用裝飾器前需要在tsconfig中開(kāi)啟experimentalDecorators屬性
類裝飾器
類裝飾器是應(yīng)用于類的構(gòu)造函數(shù)的函數(shù),它可以用來(lái)修改類的行為。類裝飾器可以有一個(gè)參數(shù),即類的構(gòu)造函數(shù),通過(guò)這個(gè)參數(shù)我們可以對(duì)類的行為進(jìn)行修改。
基本用法
類裝飾器的語(yǔ)法是在一個(gè)普通的函數(shù)名前面加上@符號(hào),后面緊跟著要裝飾的類的聲明,如:
const nameDecorator = (constructor: typeof Animal) => {
console.log(constructor.prototype.name)// undefined
}
@nameDecorator
class Animal {
name: string = "阿黃"
constructor() {
console.log(this.name);// 阿黃
}
}
new Animal()
在上述代碼中,我使用decorator獲取Animal類的name屬性,發(fā)現(xiàn)獲取的是未定義,而在構(gòu)造函數(shù)中卻可以獲取,原因是類的裝飾器是在類定義時(shí)對(duì)類進(jìn)行操作的,而屬性及函數(shù)的初始化是當(dāng)類實(shí)例化時(shí)進(jìn)行的,所以獲取不到name的值
操作方式
通過(guò)類裝飾器操作類的方式有兩種:操作類的原型和類的繼承
操作類的原型
ES6之前的類是通過(guò)構(gòu)造函數(shù)實(shí)現(xiàn)的,其原型prototype屬性是存在的,所以我們?cè)趯?duì)類進(jìn)行操作時(shí)可以使用修改原型的方式
type IAnimal = {
name?: string
getName?: () => string
}
const nameDecorator = (constructor: Function) => {
const _this = constructor.prototype // 模擬類內(nèi)部環(huán)境
_this.name = "阿黃"
_this.getName = () => {
return _this.name
}
}
@nameDecorator
class Animal implements IAnimal { }
const animal: IAnimal = new Animal()
console.log(animal.getName()) // 阿黃
上面代碼實(shí)現(xiàn)了對(duì)類中name屬性初始化以及實(shí)現(xiàn)了類的getName方法,在ES5中如何實(shí)現(xiàn)類的重寫?看看下面代碼對(duì)裝飾器的修改:
const nameDecorator = (constructor: IAnimalProto) => {
const _this = constructor.prototype // 模擬類內(nèi)部環(huán)境
_this.name = "阿黃"
return class extends constructor {
getName = () => {
return "名字:" + _this.name
}
}
}
tips:如果通過(guò)這種方式無(wú)法修改類的屬性或方法,可以把tsconfig中target屬性調(diào)整為ES5,兼容低版本瀏覽器,此時(shí)類是通過(guò)構(gòu)造函數(shù)實(shí)現(xiàn)的
類繼承操作
ES6中的類語(yǔ)法糖中沒(méi)有prototype屬性,所以我們可以使用繼承的方式實(shí)現(xiàn)上面的代碼,并使用重寫的方式修改類中的同名函數(shù)
type IAnimal = {
name?: string
getName?: () => string
}
type IAnimalProto = {
new(): Animal
} & IAnimal
const nameDecorator = (constructor: IAnimalProto) => {
return class extends constructor {
constructor(public name = "阿黃") {
super()
}
getName() {// 重寫類中的函數(shù)
return "姓名:" + this.name
}
}
}
@nameDecorator
class Animal implements IAnimal {
name?: string;
getName() {
return this.name
}
}
const animal: IAnimal = new Animal()
console.log(animal.getName()) // 姓名:阿黃
方法裝飾器
方法裝飾器是應(yīng)用于類方法的函數(shù),它可以用來(lái)修改方法的行為。方法裝飾器可以接收三個(gè)參數(shù),分別是目標(biāo)類的原型對(duì)象(若裝飾的是靜態(tài)方法,則指的是類本身)、方法名稱和方法描述符。
需要注意的是構(gòu)造函數(shù)不是類的方法,所以方法裝飾器不能直接用于裝飾構(gòu)造函數(shù)
tips:在類中使用方法裝飾器需要避免箭頭函數(shù)的出現(xiàn),因?yàn)榧^函數(shù)的this指向它定義的環(huán)境,而不是實(shí)例對(duì)象,這導(dǎo)致了它無(wú)法獲取到類的屬性和方法
下面是一個(gè)案例
const nameDecorator = (target: Animal, key: string, descriptor: PropertyDescriptor): PropertyDescriptor => {
console.log(target); // { setName: [Function (anonymous)] }
console.log(key); // setName
console.log(descriptor);
// {
// value: [Function (anonymous)],
// writable: true,
// enumerable: true,
// configurable: true
// }
return descriptor
}
class Animal {
name: string
@nameDecorator
setName(name: string) {
this.name = name
}
}
const animal = new Animal()
animal.setName("阿黃")
console.log(animal.name); // 阿黃
其中target表示當(dāng)前函數(shù)所在的類,key一般指函數(shù)名,descriptor指當(dāng)前函數(shù)對(duì)象的描述符
基于上面的代碼我們可以重寫一下類中的setName函數(shù):
const nameDecorator = (target: Animal, _: string, descriptor: PropertyDescriptor): PropertyDescriptor => {
descriptor.value = (name: string) => {
target.name = "名字:" + name
}
return descriptor
}
屬性裝飾器
屬性裝飾器應(yīng)用于類屬性的函數(shù),它可以用來(lái)修改屬性的行為或攔截屬性的定義和描述符的訪問(wèn),但是不能修改屬性值。屬性裝飾器可以接收兩個(gè)參數(shù),分別是目標(biāo)類的原型對(duì)象(若裝飾的是靜態(tài)屬性,則指的是類本身)和屬性名稱。
與方法裝飾器不同屬性裝飾器不會(huì)返回descriptor這個(gè)參數(shù),也就是無(wú)法獲取到屬性的描述
const nameDecorator = (target: Animal, key: string) => {
target[key] = '阿黃'
}
class Animal {
@nameDecorator
name: string
setName(name: string) {
this.name = name
}
}
const animal = new Animal()
console.log(animal.name);// 阿黃
animal.setName("小黑")
console.log(animal.name);// 小黑
tips:為什么無(wú)法使用屬性裝飾器給屬性定義初始值? 此時(shí)可以檢查一下你的tsconfig.json里面的配置target是不是設(shè)置成ES2021以后(如:ESNext,ES2022,ES2023等),在ES2022前在TS中聲明了屬性成員會(huì)在JS編譯成第一張圖,ES2022及以后顯示的是第二張圖


解決方法:參考這個(gè)

我們可以將屬性聲明修改為環(huán)境聲明(declare,在后續(xù)文章會(huì)說(shuō)到),或者使用舊版本的target配置
class Animal {
@nameDecorator
declare name: string
setName(name: string) {
this.name = name
}
}
存取器裝飾器
存取器裝飾器是一種特殊類型的裝飾器,它可以被用來(lái)裝飾類中的存取器屬性。它的使用方式與方法裝飾器相同。但是與方法裝飾器不同的是存取器descriptor參數(shù)中沒(méi)有value值,但是我們可以通過(guò)修改descriptor來(lái)重寫getter和setter方法
const nameDecorator = (_: any, __: string, descriptor: PropertyDescriptor) => {
const __getter = descriptor.get;
descriptor.get = function () {// 必須使用function,使用箭頭函數(shù)獲取不到this
const value = __getter?.call(this);// 運(yùn)行g(shù)et獲取存取器屬性
return "名字:" + value;
};
}
class Animal {
constructor(private _name: string) { }
@nameDecorator
get name() {
return this._name
}
}
const animal = new Animal("阿黃")
console.log(animal.name);
參數(shù)裝飾器
參數(shù)裝飾器是應(yīng)用于類構(gòu)造函數(shù)或方法參數(shù)的函數(shù),它可以用來(lái)獲取參數(shù)位置。參數(shù)裝飾器可以接收三個(gè)參數(shù),分別是目標(biāo)類的原型對(duì)象(若裝飾的是靜態(tài)方法,則指的是類本身)、方法名稱和參數(shù)索引(第幾個(gè)參數(shù),從0開(kāi)始)。
基本用法
參數(shù)裝飾器是一個(gè)函數(shù),它可以被應(yīng)用到類的構(gòu)造函數(shù)、方法的參數(shù)上,它無(wú)法應(yīng)用在訪問(wèn)器(getter 和 setter)的參數(shù)。
const nameDecorator = (target: any, key: string, parameterIndex: number) => {
const name = target.name ?? target.constructor.name
console.log(`${name}中的${key ?? '構(gòu)造函數(shù)'}第${parameterIndex}個(gè)參數(shù)`);
}
class Animal {
constructor(public _name: string) { }
setName(@nameDecorator name: string) {
this._name = name
}
}
new Animal("阿黃")
參數(shù)裝飾器雖然無(wú)法直接獲取或者修改參數(shù),但是可以將參數(shù)的位置標(biāo)識(shí)出來(lái),與元數(shù)據(jù)(reflect-metadata庫(kù)),以及方法裝飾器配合達(dá)到過(guò)濾參數(shù)的目的
參數(shù)過(guò)濾器
下面我們借助一個(gè)簡(jiǎn)單的反射元數(shù)據(jù)操作實(shí)現(xiàn)一個(gè)參數(shù)過(guò)濾器
元數(shù)據(jù)函數(shù)實(shí)現(xiàn)
type IKey = string | symbol | number
const getReflect = () => Reflect ?? Object
const __Reflect = getReflect()
const defineMeta = (target: any, key: IKey, metadataKey: IKey, descriptor: PropertyDescriptor): void => {
__Reflect.defineProperty(target[key], metadataKey, descriptor)
}
const getMeta = (target: any, key: IKey, metadataKey: IKey,): PropertyDescriptor => {
return __Reflect.getOwnPropertyDescriptor(target[key], metadataKey)
}
參數(shù)過(guò)濾
下面我們實(shí)現(xiàn)一個(gè)參數(shù)過(guò)濾,如果name等于阿黃,則中斷函數(shù)執(zhí)行并跳出
// 存儲(chǔ)參數(shù)的索引
const saveMeta2Arr = (target: any, key: string, parameterIndex: number, keyWord: string) => {
const paramsList = getMeta(target, key, keyWord)?.value ?? []
paramsList.push(parameterIndex)
defineMeta(target, key, keyWord, { value: paramsList })
return paramsList
}
// 參數(shù)裝飾器
const paramsDecorator = (target: any, key: string, parameterIndex: number) => {
const paramsList: string[] = saveMeta2Arr(target, key, parameterIndex, 'list:params')// 參數(shù)列表
defineMeta(target, key, 'filter:params', {
value: (...args) => {
if (!!!args.length) return void 0 // 沒(méi)傳參數(shù)默認(rèn)跳過(guò)參數(shù)校驗(yàn)
return paramsList.filter(it => args[it] === "阿黃").length > 0// 我的校驗(yàn)規(guī)則是參數(shù)等于阿黃就跳出函數(shù),這個(gè)可以自行修改
}
})
}
// 函數(shù)裝飾器
const methodDecorator = (target: Animal, key: string, descriptor: PropertyDescriptor) => {
const fn = getMeta(target, key, 'filter:params').value // 獲取參數(shù)裝飾器的回調(diào)函數(shù)
const method = descriptor.value
descriptor.value = function (...args) {
if (fn(...args)) return console.error("跳出了函數(shù)");// 過(guò)濾操作
method.apply(this, args)
}
}
class Animal {
constructor(public name?: string) { }
@methodDecorator
setInfo(@paramsDecorator name?: string) {
console.log("執(zhí)行了函數(shù)");
this.name = name
}
}
效果實(shí)踐
實(shí)現(xiàn)完成我們實(shí)例化一下類試試,可以看到,此時(shí)由于我們傳入的參數(shù)是阿黃,所以setName函數(shù)未執(zhí)行
const animal = new Animal()
animal.setInfo("阿黃")
console.log(animal.name);
// 跳出了函數(shù)
// undefined
下面我們修改一下,傳入一個(gè)參數(shù):小黑
const animal = new Animal()
animal.setInfo("小黑")
console.log(animal.name);
// 執(zhí)行了函數(shù)
// 小黑
上述代碼執(zhí)行了setName并且將name賦值了小黑
裝飾器優(yōu)先級(jí)
下面說(shuō)說(shuō)當(dāng)多個(gè)裝飾器作用于同一個(gè)目標(biāo)時(shí),它們執(zhí)行的順序和影響的優(yōu)先級(jí)是怎樣的
相同裝飾器
同一種裝飾器的執(zhí)行順序是從下往上,理解為就近原則,近的先執(zhí)行
const decorator1 = (...args: any[]) => {
console.log(1)
}
const decorator2 = (...args: any[]) => {
console.log(2)
}
const decorator3 = (...args: any[]) => {
console.log(3)
}
@decorator1
@decorator2
@decorator3
class Animal { }
new Animal()
// 輸出3 2 1
不同裝飾器
不同裝飾器遵循:參數(shù)>函數(shù)=屬性=存取器>類,參數(shù)優(yōu)先級(jí)最高,類最后執(zhí)行。何以見(jiàn)得?
const decorator1 = (...args: any[]) => {
console.log(1)
}
const decorator2 = (...args: any[]) => {
console.log(2)
}
const decorator3 = (...args: any[]) => {
console.log(3)
}
const decorator4 = (...args: any[]) => {
console.log(4)
}
const decorator5 = (...args: any[]) => {
console.log(5)
}
@decorator1
class Animal {
@decorator2
_name: string
@decorator3
get name() {
return this._name
}
@decorator4
setName(@decorator5 name: string) {
this._name = name
}
}
new Animal()
上面的代碼輸出2 3 5 4 1;我們換個(gè)順序,把屬性,函數(shù),存取器調(diào)換位置再試試
@decorator1
class Animal {
@decorator4
setName(@decorator5 name: string) {
this._name = name
}
@decorator3
get name() {
return this._name
}
@decorator2
_name: string
}
輸出5 4 3 2 1。可見(jiàn)屬性,函數(shù),存取器優(yōu)先級(jí)在同級(jí),參數(shù)更高,類更低
裝飾器工廠
使用類裝飾器和方法裝飾器時(shí),我們難免會(huì)遇到參數(shù)傳遞的問(wèn)題,每個(gè)裝飾器都有可以復(fù)用的可能,為了使代碼高可用,我們可以嘗試使用高階函數(shù)實(shí)現(xiàn)一個(gè)工廠,外部函數(shù)接收參數(shù),函數(shù)返回裝飾器
type IAnimal = {
name?: string
}
type IAnimalProto = {
new(name: string): Animal
} & IAnimal
const nameDecorator = (name: string) => (constructor: IAnimalProto) => {
return class extends constructor {
constructor() {
super(name)
}
}
}
@nameDecorator("阿黃")
class Animal implements IAnimal {
constructor(public name?: string) { }
}
const animal: IAnimal = new Animal()
console.log(animal.name);
上述代碼中我們實(shí)現(xiàn)了使用裝飾器工廠給Animal類的屬性name賦予默認(rèn)值的功能
hooks與class兼容
在實(shí)際開(kāi)發(fā)中,如果是使用react開(kāi)發(fā)應(yīng)該會(huì)遇到這樣的問(wèn)題,使用類開(kāi)發(fā)的組件裝飾器寫法是這樣
@EnhanceConnect((state: any) => ({
global: state['@global'].data
}))
export class MyComponent {
constructor(props: IProps) {
// ...
}
}
如何轉(zhuǎn)換成hooks寫法?
export const MyComponent = EnhanceConnect((state: any) => ({
global: state['@global'].data
}))((props: IProps) => {
useEffect(() => {
// ...
})
})
結(jié)語(yǔ)
本文詳細(xì)講述了類裝飾器,方法裝飾器,屬性裝飾器,存取器裝飾器,參數(shù)裝飾器這五種裝飾器的基本用法及注意事項(xiàng);此外還針對(duì)裝飾器優(yōu)先級(jí)進(jìn)行了排序及證明;最后介紹了裝飾器工廠即裝飾器的實(shí)際應(yīng)用場(chǎng)景兼容方法。
感謝你的閱讀,希望文章能對(duì)你有幫助,有任何問(wèn)題歡迎留言私信,請(qǐng)別忘了給作者點(diǎn)個(gè)贊哦,感謝~

浙公網(wǎng)安備 33010602011771號(hào)