TypeScript 裝飾器


裝飾器(Decorators)可用來裝飾類,屬性,及方法,甚至是函數的參數,以改變和控制這些對象的表現,獲得一些功能。

裝飾器以 @expression 形式呈現在被裝飾對象的前面或者上方,其中 expression 為一個函數,根據其所裝飾的對象的不同,得到的入參也不同。

以下兩種風格均是合法的:

@f @g x
@f
@g
x

ES 中裝飾器處於 Stage 2 階段 ,TypeScript 中通過開啟相應編譯開關來使用。

{
  "compilerOptions": {
    "target": "ES5",
    "experimentalDecorators": true
  }
}

一個簡單的示例

一個簡單的示例,展示了 TypeScript 中如何編寫和使用裝飾器。

function log(
  _target: any,
  propertyKey: string,
  descriptor: PropertyDescriptor
) {
  const originalMethod = descriptor.value;
  descriptor.value = function() {
    console.log(`method ${propertyKey} called`);
    return originalMethod.apply(this, arguments);
  };
}

class Test {
@log
static sayHello() {
console.log("hello");
}
}

Test.sayHello();

上面的示例中,創建了名為 log 的方法,它將作為裝飾器作用於類的方法上,在方法被調用時輸出一條日志。作為裝飾器的 log 函數其入參在后面會介紹。

執行結果:

method sayHello called
hello

裝飾器的工廠方法

上面的裝飾器比較呆板,設想我們想將它變得更加靈活和易於復用一些,則可以通過創建一個工廠方法來實現。因為本質上裝飾器就是個普通函數,函數可通過另外的函數來創建和返回,同時裝飾器的使用本質上也是一個函數調用。通過傳遞給工廠方法不同的參數,以獲得不同表現的裝飾器。

function logFactory(prefix: string) {
  return function log(
    _target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    const originalMethod = descriptor.value;
    descriptor.value = function() {
      console.log(`method ${propertyKey} called`);
      return originalMethod.apply(this, arguments);
    };
  };
}

class Test {
@logFactory("[debug]")
static sayHello() {
console.log("hello");
}
@logFactory("[info]")
static sum() {
return 1 + 1;
}
}

Test.sayHello();
Test.sum();

執行結果:

[debug] method sayHello called
hello
[info] method sum called

多個裝飾器

多個裝飾器可同時作用於同一對象,按順序書寫出需要運用的裝飾器即可。其求值(evaluate)和真正被執行(call)的順序是反向的。即,排在前面的先求值,排在最后的先執行。

譬如,

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() {}
}

求值 的過程就體現在裝飾器可能並不直接是一個可調用的函數,而是一個工廠方法或其他表達式,只有在這個工廠方法或表達式被求值后,才得到真正被調用的裝飾器。

所以在這個示例中,先依次對 f() g() 求值,再從 g() 開始執行到 f()

運行結果:

f(): evaluated
g(): evaluated
g(): called
f(): called

不同類型的裝飾器

類的裝飾器

作用於類(Class)上的裝飾器,用於修改類的一些屬性。如果裝飾器有返回值,該返回值將替換掉該類的聲明而作為新的構造器使用。

裝飾器入參:

  • 類的構造器。

示例:

function sealed(constructor: Function) {
  Object.seal(constructor);
  Object.seal(constructor.prototype);
}

@sealed
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
return "Hello, " + this.greeting;
}
}

@sealed 將類進行密封,將無法再向類添加屬性,同時類上屬性也變成不可配置的(non-configurable)。

另一個示例:

function classDecorator<T extends { new (...args: any[]): {} }>(
  constructor: T
) {
  return class extends constructor {
    newProperty = "new property";
    hello = "override";
  };
}

@classDecorator
class Greeter {
property = "property";
hello: string;
constructor(m: string) {
this.hello = m;
}
}

console.log(new Greeter("world"));

因為 @classDecorator 中有返回值,這個值將替換本來類的定義,當 new 的時候,使用的是裝飾器中返回的構造器來創建類。

方法的裝飾器

裝飾器作用於類的方法時可用於觀察,修改或替換該方法。如果裝飾器有返回值,將替換掉被作用方法的屬性描述器(roperty Descriptor)。

裝飾器入參依次為:

  • 作用於靜態方法時為類的構造器,實例方法時為類的原型(prototype)。
  • 被作用的方法的名稱。
  • 被作用對象的屬性描述器。

示例:

function enumerable(value: boolean) {
  return function(
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    descriptor.enumerable = value;
  };
}

class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}

@enumerable(false)
greet() {
return "Hello, " + this.greeting;
}
}

上面示例中 @enumerable 改變了被裝飾方法的 enumerable 屬性,控制其是否可枚舉。

類的方法可以是設置器(setter)或獲取器(getter)。當兩者成對出現時,應當只對其中一個運用裝飾器,誰先出現就用在誰身上。因為裝飾器應用時是用在 getset 兩者合並的屬性描述器上的。

class Test {
  private _foo = 1;
  @logFactory("[info]")
  get foo() {
    return this._foo;
  }
  //🚨 Decorators cannot be applied to multiple get/set accessors of the same name.ts(1207)
  @logFactory("[info]")
  set foo(val: number) {
    this._foo = val;
  }
}

屬性的裝飾器

作用於類的屬性時,其入參依次為:

  • 如果裝飾的是靜態屬性則為類的構造器,實例屬性則為類的原型
  • 屬性名

此時並沒有提供第三個入參,即該屬性的屬性描述器。因為定義屬性時,沒有相應機制來描述該屬性,同時屬性初始化時也沒有方式可以對其進行修改或觀察。

如果裝飾器有返回值,將被忽略。

因此,屬性裝飾器僅可用於觀察某個屬性是否被創建。

一個示例:

function logProperty(target: any, key: string) {
  // property value
  var _val = this[key];

// property getter
var getter = function() {
console.log(</span>Get: ${<span class="pl-smi">key</span>} =&gt; ${<span class="pl-smi">_val</span>}<span class="pl-pds">);
return _val;
};

// property setter
var setter = function(newVal) {
console.log(</span>Set: ${<span class="pl-smi">key</span>} =&gt; ${<span class="pl-smi">newVal</span>}<span class="pl-pds">);
_val = newVal;
};

// Delete property.
if (delete this[key]) {
// Create new property with getter and setter
Object.defineProperty(target, key, {
get: getter,
set: setter,
enumerable: true,
configurable: true
});
}
}

class Person {
@logProperty
public name: string;
public surname: string;

constructor(name: string, surname: string) {
this.name = name;
this.surname = surname;
}
}

var p = new Person("remo", "Jansen");
p.name = "Remo";
var n = p.name;

這個示例中,通過將原屬性刪除,創建帶設置器和獲取器的同名屬性,來達到對屬性值變化的監聽。注意此時操作的已經不是最初那個屬性了。

運行結果:

Set: name => remo
Set: name => Remo
Get: name => Remo

參數的裝飾器

裝飾器也可作用於方法的入參,這個方法不僅限於類的成員方法,還可以是類的構造器。裝飾器的返回值會被忽略。

當作用於方法的參數時,裝飾器的入參依次為:

  • 如果裝飾的是靜態方法則為類的構造器,實例方法則為類的原型。
  • 被裝飾的參數名。
  • 參數在參數列表中的索引。

比如,定義一個參數為必傳的:

import "reflect-metadata";

const requiredMetadataKey = Symbol("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
);
}

function validate(
target: any,
propertyName: string,
descriptor: TypedPropertyDescriptor<Function>
) {
let method = descriptor.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
) {
throw new Error("Missing required argument.");
}
}
}

<span class="pl-k">return</span> <span class="pl-smi">method</span>.<span class="pl-c1">apply</span>(<span class="pl-c1">this</span>, <span class="pl-c1">arguments</span>);

};
}

class Greeter {
greeting: string;

constructor(message: string) {
this.greeting = message;
}

@validate
greet(@required name: string) {
return "Hello " + name + ", " + this.greeting;
}
}

上面示例中,@required 將參數標記為必需,配合 @validate 在調用真實的方法前進行檢查。

相關資源


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM