概念介紹
裝飾器模式(Decorator Pattern)允許像一個現(xiàn)有的對象添加新的功能,同時又不改變其結(jié)構(gòu)义钉,這種類型的設(shè)計(jì)模式屬于結(jié)構(gòu)型模式蕊玷,他是作為現(xiàn)有的類的一個包裝。
這種模式創(chuàng)建了一個裝飾類,用來包裝原有的類严沥,并在保持類方法簽名的完整性的前提下,提供了額外的功能中姜。
我們通過下面的實(shí)例來演示裝飾器模式的用法消玄。其中,我們把一個形狀裝飾上不同的顏色丢胚,同時又不改變形狀類
用js實(shí)現(xiàn)一個裝飾器翩瓜,看下原理
const dec = (target, property) => {
const old = target.prototype[property];
target.prototype[property] = (msg) => {
msg = `【${msg}】`;
old(msg)
}
}
class Log {
print(str: string) {
console.log(str);
}
}
dec(Log, "print");
const log = new Log()
log.print("你好")
在控制臺里會打印出如下結(jié)果:
【你好】
關(guān)于類、方法携龟、訪問器兔跌、屬性裝飾器(Typescript中)
- 類裝飾器:接收1個參數(shù)
(target)
、可以用來監(jiān)視峡蟋、修改和替換類定義 - 方法裝飾器:接收3個參數(shù)
(target坟桅、property、descriptor)
可以用來監(jiān)視蕊蝗、修改和替換方法定義 - 訪問器裝飾器:接收3個參數(shù)
(target仅乓、property、descriptor)
可以用來監(jiān)視蓬戚、修改和替換訪問器定義 - 屬性裝飾器:接收2個參數(shù)
(target夸楣、property)
只能用來監(jiān)視類中是否聲明了某個名字的屬性 - 參數(shù)裝飾器:接收3個參數(shù)
(target、property子漩、paramsIndex)
只能用來監(jiān)視一個方法的參數(shù)是否被傳入豫喧。
詳解:
target:對于靜態(tài)成員來說是類的構(gòu)造函數(shù),對于實(shí)例成員是類的原型對象(prototype)
property:成員的名字
descriptor:成員的屬性描述符
paramsIndex:參數(shù)的下標(biāo)
descriptor簡單介紹
//descriptor 屬性描述符
//在方法裝飾器中
{
value: [Function],
writable: true,
enumerable: true,
configurable: true
}
//在訪問器裝飾器中
{
get: [Function: get],
set: undefined,
enumerable: false,
configurable: true
}
類裝飾器
類裝飾器應(yīng)用于類構(gòu)造函數(shù)幢泼,可以用來監(jiān)視嘿棘,修改或替換類定義。
注意?類的構(gòu)造函數(shù)作為其唯一的參數(shù)旭绒,如果類裝飾器返回一個值鸟妙,他會使用提供的構(gòu)造函數(shù)來替換類的聲明
const anotation: Function = (target) => {
console.log(target.foo);
console.log(target.method);
return class extends target {
newMethod() {
console.log("Example New Method");
}
}
}
@anotation
class Example {
static foo = "fooValue";
public method() {
console.log("Example Public Method");
}
}
const example = new Example();
example["newMethod"]();
在控制臺里會打印出如下結(jié)果:
fooValue
undefined
Example New Method
方法裝飾器
用來監(jiān)視,修改或者替換方法定義挥吵。
方法裝飾器表達(dá)式會在運(yùn)行時當(dāng)作函數(shù)被調(diào)用重父,傳入下列3個參數(shù)(target,property,descriptor):
- target:對于靜態(tài)成員來說是類的構(gòu)造函數(shù)(
包含全部的靜態(tài)屬性
),對于實(shí)例成員是類的原型對象(包含全部的實(shí)例屬性
)忽匈。 - property:成員的名字房午。
- descriptor:成員的屬性描述符。
第一種return target
裝飾函數(shù)(想使用target裝飾函數(shù)丹允,必須return)
// 方法裝飾器(return target)
const anotationMethod: Function = (target, property, descriptor) => {
const old = target[property];
target[property] = function () {
console.log("old calling");
old();
}
return target
}
class Example {
static foo = "fooValue";
@anotationMethod
public method() {
console.log("Example Public Method");
}
}
const example = new Example();
example.method();
在控制臺里會打印出如下結(jié)果:
old calling
Example Public Method
2.第二種descriptor.value
裝飾函數(shù)(直接修改descriptor.value即可)
// 方法裝飾器(修改descriptor.vaue)
const anotationMethod: Function = (target, property, descriptor) => {
const old = descriptor.value;
descriptor.value = function () {
console.log("old calling");
old();
}
}
// 方法裝飾器(修改descriptor.value)
class Example {
static foo = "fooValue";
@anotationMethod
public method() {
console.log("Example Public Method");
}
}
const example = new Example();
example.method();
在控制臺會打印出如下結(jié)果
old calling
Example Public Method
訪問器裝飾器
訪問器裝飾器應(yīng)用于訪問器的 屬性描述符并且可以用來監(jiān)視郭厌,修改或替換一個訪問器的定義袋倔。
注意? TypeScript不允許同時裝飾一個成員的get和set訪問器。取而代之的是折柠,一個成員的所有裝飾的必須應(yīng)用在文檔順序的第一個訪問器上宾娜。這是因?yàn)椋谘b飾器應(yīng)用于一個屬性描述符時扇售,它聯(lián)合了get和set訪問器前塔,而不是分開聲明的。
訪問器裝飾器表達(dá)式會在運(yùn)行時當(dāng)作函數(shù)被調(diào)用承冰,傳入下列3個參數(shù):
- 對于靜態(tài)成員來說是類的構(gòu)造函數(shù)华弓,對于實(shí)例成員是類的原型對象。
- 成員的名字困乒。
- 成員的屬性描述符寂屏。
注意? 如果代碼輸出目標(biāo)版本小于ES5,Property Descriptor將會是undefined娜搂。
const configurable: Function = (target, property, descriptor: PropertyDescriptor) => {
console.log(target);
console.log(property);
console.log(descriptor);
descriptor.get = () => {
return "楊志強(qiáng)"
}
}
class Point {
private _name: string = "張三";
@configurable
get name() {
return this._name
}
set name(newVal) {
this._name = newVal;
}
}
const point = new Point();
console.log(point.name);
在控制臺會輸出如下結(jié)果
Point {}
name
{
get: [Function: get],
set: [Function: set],
enumerable: false,
configurable: true
}
楊志強(qiáng)
屬性裝飾器
屬性裝飾器只能用來監(jiān)視類中是否聲明了某個名字的屬性凑保。
因?yàn)槟壳皼]有辦法在定義一個原型對象的成員時描述一個實(shí)例屬性,并且沒辦法監(jiān)視或修改一個屬性的初始化方法涌攻。返回值也會被忽略。
屬性裝飾器表達(dá)式會在運(yùn)行時當(dāng)作函數(shù)被調(diào)用频伤,傳入下列倆個參數(shù)
- 對于靜態(tài)成員來說是類的構(gòu)造函數(shù)恳谎,對于實(shí)例成員是類的原型對象。
- 成員的名字憋肖。
注意 屬性描述符不會作為參數(shù)傳入屬性裝飾器因痛,這與Typescript是如何初始化屬性裝飾器有關(guān)
import "reflect-metadata"
const formatMetadataKey = Symbol("format");
const format = (value: string) => {
return Reflect.metadata(formatMetadataKey, value);
}
function getFormat(target, property) {
return Reflect.getMetadata(formatMetadataKey, target, property);
}
class Point {
@format("Hello World")
public name: string = "張三";
greet(property) {
return getFormat(this, property);
}
}
const point = new Point();
console.log(point.greet("name"));
這個@format("Hello World")
裝飾器是個 裝飾器工廠。 當(dāng) @format("Hello World")
被調(diào)用時岸更,它會添加一條這個屬性的元數(shù)據(jù)鸵膏,通過reflect-metadata
庫里的Reflect.metadata
函數(shù)。 當(dāng) getFormat
被調(diào)用時怎炊,它讀取格式的元數(shù)據(jù)谭企。
在控制臺會打印如下結(jié)果
Hello World
參數(shù)裝飾器
參數(shù)裝飾器只能用來監(jiān)視一個方法的參數(shù)是否被傳入。
參數(shù)裝飾器表達(dá)式會在運(yùn)行時當(dāng)作函數(shù)被調(diào)用评肆,傳入下列3個參數(shù):
- 對于靜態(tài)成員來說是類的構(gòu)造函數(shù)债查,對于實(shí)例成員是類的原型對象。
- 成員的名字瓜挽。
- 參數(shù)在函數(shù)參數(shù)列表中的索引盹廷。
import "reflect-metadata";
const requiredKey = Symbol("requeiredKey")
const required: Function = (target, property, paramIndex) => {
let paramIndexs = Reflect.getMetadata(requiredKey, target, property) || [];
paramIndexs.push(paramIndex);
Reflect.defineMetadata(requiredKey, paramIndexs, target, property);
}
// 此處為什么能用getOwnMetadata也能接收到?
// 答:因?yàn)檠b飾器接收的target對于實(shí)例成員來說類的原型對象(prototype),對于靜態(tài)成員來說是類的構(gòu)造函數(shù)久橙,所以我們r(jià)equired和validate指向同一個對象(target.prototype),所以能獲取元數(shù)據(jù)
const validate: Function = (target, property, descriptor: PropertyDescriptor) => {
const method = descriptor.value;
descriptor.value = function () {
const requeiredParamers = Reflect.getOwnMetadata(requiredKey, target, property);
if (requeiredParamers) {
for (let paramIndex of requeiredParamers) {
if (paramIndex >= arguments.length || arguments[paramIndex] === undefined) {
throw new Error("參數(shù)不正確");
}
}
}
method.apply(this, arguments);
}
}
class User {
name: string = "張三"
@validate
info(@required id?: number) {
console.log(`用戶id為${id}`);
}
}
const user = new User();
user.info(1);
在控制臺里面打印會打印如下結(jié)果
用戶id為1
關(guān)于元數(shù)據(jù)方法的補(bǔ)充
Reflect.getMetadata(metadataKey,target,property):獲取目標(biāo)對象或其原型鏈上提供的元數(shù)據(jù)鍵的元數(shù)據(jù)值俄占。
Reflect.getOwnMetadata(metadataKey,target,property)::獲取目標(biāo)對象上提供的元數(shù)據(jù)鍵的元數(shù)據(jù)值(目標(biāo)對象指的不是this管怠,而是未實(shí)例化的這個類)。
Reflect.defineMetadata(metadataKey,value,target,property):在目標(biāo)上定義唯一的元數(shù)據(jù)項(xiàng)缸榄。
Reflect.metadata(metadataKey,value):可用于類渤弛、類成員或參數(shù)的默認(rèn)元數(shù)據(jù)裝飾器工廠。(一般直接return)
關(guān)于裝飾器的執(zhí)行順序
如果我們使用裝飾器工廠的話碰凶,可以通過下面的例子來觀察它們求值的順序:
function f() {
console.log("f(): evaluated");
return function (target, propertyKey: string, descriptor: PropertyDescriptor) {
console.log("f(): called");
}
}
function g() {
console.log("g(): evaluated");
return function (target, propertyKey: string, descriptor: PropertyDescriptor) {
console.log("g(): called");
}
}
class C {
@f()
@g()
method() {}
}
在控制臺里面打印會打印如下結(jié)果
f(): evaluated
g(): evaluated
g(): called
f(): called