TypeScript 裝飾器(Decorator)
裝飾器是一種特殊類型的聲明,它能夠附加到類聲明、方法、訪問符、屬性、類方法的參數(shù)上,以達(dá)到擴(kuò)展類的行為。
自從 ES2015 引入 class
,當(dāng)我們需要在多個不同的類之間共享或者擴(kuò)展一些方法或行為的時候,代碼會變得錯綜復(fù)雜,極其不優(yōu)雅,這也是裝飾器被提出的一個很重要的原因。
1. 慕課解釋
常見的裝飾器有:類裝飾器、屬性裝飾器、方法裝飾器、參數(shù)裝飾器。
裝飾器的寫法:普通裝飾器(無法傳參)、 裝飾器工廠(可傳參)。
裝飾器是一項(xiàng)實(shí)驗(yàn)性特性,在未來的版本中可能會發(fā)生改變。
若要啟用實(shí)驗(yàn)性的裝飾器特性,你必須在命令行或 tsconfig.json
里啟用 experimentalDecorators
編譯器選項(xiàng):
命令行:
tsc --target ES5 --experimentalDecorators
tsconfig.json:
{
"compilerOptions": {
"target": "ES5",
"experimentalDecorators": true
}
}
2. 裝飾器的使用方法
裝飾器允許你在類和方法定義的時候去注釋或者修改它。裝飾器是一個作用于函數(shù)的表達(dá)式,它接收三個參數(shù) target
、 name
和 descriptor
,然后可選性的返回被裝飾之后的 descriptor
對象。
裝飾器使用 @expression
這種語法糖形式,expression
表達(dá)式求值后必須為一個函數(shù),它會在運(yùn)行時被調(diào)用,被裝飾的聲明信息做為參數(shù)傳入。
2.1 裝飾器工廠
裝飾器工廠就是一個簡單的函數(shù),它返回一個表達(dá)式,以供裝飾器在運(yùn)行時調(diào)用。
通過裝飾器工廠方法,可以額外傳參,普通裝飾器無法傳參。
function log(param: string) {
return function (target: any, name: string, descriptor: PropertyDescriptor) {
console.log('target:', target)
console.log('name:', name)
console.log('descriptor:', descriptor)
console.log('param:', param)
}
}
class Employee {
@log('with param')
routine() {
console.log('Daily routine')
}
}
const e = new Employee()
e.routine()
代碼解釋:
第 1 行,聲明的 log()
函數(shù)就是一個裝飾器函數(shù),通過裝飾器工廠這種寫法,可以接收參數(shù)。
來看代碼的打印結(jié)果:
target: Employee { routine: [Function] }
name: routine
descriptor: {
value: [Function],
writable: true,
enumerable: true,
configurable: true
}
param: with param
Daily routine
可以看到,先執(zhí)行裝飾器函數(shù),然后執(zhí)行 routine()
函數(shù)。至于類屬性裝飾器函數(shù)表達(dá)式的三個參數(shù) target
、name
、descriptor
之后會單獨(dú)介紹。
2.2 裝飾器組合
多個裝飾器可以同時應(yīng)用到一個聲明上,就像下面的示例:
- 書寫在同一行上:
@f @g x
- 書寫在多行上:
@f
@g
x
在 TypeScript 里,當(dāng)多個裝飾器應(yīng)用在一個聲明上時會進(jìn)行如下步驟的操作:
- 由上至下依次對裝飾器表達(dá)式求值
- 求值的結(jié)果會被當(dāng)作函數(shù),由下至上依次調(diào)用
通過下面的例子來觀察它們求值的順序:
function f() {
console.log('f(): evaluated');
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
console.log('f(): called');
}
}
function g() {
console.log('g(): evaluated');
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
console.log('g(): called');
}
}
class C {
@f()
@g()
method() {}
}
在控制臺里會打印出如下結(jié)果:
f(): evaluated
g(): evaluated
g(): called
f(): called
3. 類裝飾器
類裝飾器表達(dá)式會在運(yùn)行時當(dāng)作函數(shù)被調(diào)用,類的構(gòu)造函數(shù)作為其唯一的參數(shù)。
通過類裝飾器擴(kuò)展類的屬性和方法:
function extension<T extends { new(...args:any[]): {} }>(constructor: T) {
// 重載構(gòu)造函數(shù)
return class extends constructor {
// 擴(kuò)展屬性
public coreHour = '10:00-15:00'
// 函數(shù)重載
meeting() {
console.log('重載:Daily meeting!')
}
}
}
@extension
class Employee {
public name!: string
public department!: string
constructor(name: string, department: string) {
this.name = name
this.department = department
}
meeting() {
console.log('Every Monday!')
}
}
let e = new Employee('Tom', 'IT')
console.log(e) // Employee { name: 'Tom', department: 'IT', coreHour: '10:00-15:00' }
e.meeting() // 重載:Daily meeting!
函數(shù)表達(dá)式的寫法:
const extension = (constructor: Function) => {
constructor.prototype.coreHour = '10:00-15:00'
constructor.prototype.meeting = () => {
console.log('重載:Daily meeting!');
}
}
@extension
class Employee {
public name!: string
public department!: string
constructor(name: string, department: string) {
this.name = name
this.department = department
}
meeting() {
console.log('Every Monday!')
}
}
let e: any = new Employee('Tom', 'IT')
console.log(e.coreHour) // 10:00-15:00
e.meeting() // 重載:Daily meeting!
代碼解釋:
以上兩種寫法,其實(shí)本質(zhì)是相同的,類裝飾器函數(shù)表達(dá)式將構(gòu)造函數(shù)作為唯一的參數(shù),主要用于擴(kuò)展類的屬性和方法。
4. 作用于類屬性的裝飾器
作用于類屬性的裝飾器表達(dá)式會在運(yùn)行時當(dāng)作函數(shù)被調(diào)用,傳入下列3個參數(shù) target
、name
、descriptor
:
target
: 對于靜態(tài)成員來說是類的構(gòu)造函數(shù),對于實(shí)例成員是類的原型對象name
: 成員的名字descriptor
: 成員的屬性描述符
如果你熟悉 Object.defineProperty
,你會立刻發(fā)現(xiàn)這正是 Object.defineProperty 的三個參數(shù)。
比如通過修飾器完成一個屬性只讀功能,其實(shí)就是修改數(shù)據(jù)描述符中的 writable
的值 :
function readonly(value: boolean) {
return function (target: any, name: string, descriptor: PropertyDescriptor) {
descriptor.writable = value
}
}
class Employee {
@readonly(false)
salary() {
console.log('這是個秘密')
}
}
const e = new Employee()
e.salary = () => { // Error,不可寫
console.log('change')
}
e.salary()
解釋: 因?yàn)?readonly
裝飾器將數(shù)據(jù)描述符中的 writable
改為不可寫,所以倒數(shù)第三行報錯。
5. 方法參數(shù)裝飾器
參數(shù)裝飾器表達(dá)式會在運(yùn)行時當(dāng)作函數(shù)被調(diào)用,以使用參數(shù)裝飾器為類的原型上附加一些元數(shù)據(jù),傳入下列3個參數(shù) target
、name
、index
:
target
: 對于靜態(tài)成員來說是類的構(gòu)造函數(shù),對于實(shí)例成員是類的原型對象name
: 成員的名字index
: 參數(shù)在函數(shù)參數(shù)列表中的索引
注意第三個參數(shù)的不同。
function log(param: string) {
console.log(param)
return function (target: any, name: string, index: number) {
console.log(index)
}
}
class Employee {
salary(@log('IT') department: string, @log('John') name: string) {
console.log('這是個秘密')
}
}
可以用參數(shù)裝飾器來監(jiān)控一個方法的參數(shù)是否被傳入。
6. 裝飾器執(zhí)行順序
function extension(params: string) {
return function (target: any) {
console.log('類裝飾器')
}
}
function method(params: string) {
return function (target: any, name: string, descriptor: PropertyDescriptor) {
console.log('方法裝飾器')
}
}
function attribute(params: string) {
return function (target: any, name: string) {
console.log('屬性裝飾器')
}
}
function argument(params: string) {
return function (target: any, name: string, index: number) {
console.log('參數(shù)裝飾器', index)
}
}
@extension('類裝飾器')
class Employee{
@attribute('屬性裝飾器')
public name!: string
@method('方法裝飾器')
salary(@argument('參數(shù)裝飾器') name: string, @argument('參數(shù)裝飾器') department: string) {}
}
查看運(yùn)行結(jié)果:
屬性裝飾器
參數(shù)裝飾器 1
參數(shù)裝飾器 0
方法裝飾器
類裝飾器
7. 小結(jié)
雖然裝飾器還在草案階段,但借助 TypeScript 與 Babel(需安裝 babel-plugin-transform-decorators-legacy
插件) 這樣的工具已經(jīng)被應(yīng)用于很多基礎(chǔ)庫中,當(dāng)需要在多個不同的類之間共享或者擴(kuò)展一些方法或行為時,可以使用裝飾器簡化代碼。