Mohan Ram 原做,受權 New Frontend 翻譯。javascript
裝飾器讓程序員能夠編寫元信息之內省代碼。裝飾器的最佳使用場景是橫切關注點——面向切面編程。php
面向切面編程(AOP) 是一種編程範式,它容許咱們分離橫切關注點,藉此達到增長模塊化程度的目標。它能夠在不修改代碼自身的前提下,給已有代碼增長額外的行爲(通知)。java
@log // 類裝飾器
class Person {
constructor(private firstName: string, private lastName: string) {}
@log // 方法裝飾器
getFullName() {
return `${this.firstName} ${this.lastName}`;
}
}
const person = new Person('Mohan', 'Ram');
person.getFullName();
複製代碼
上面的代碼展現了裝飾器多麼具備聲明性。下面咱們將介紹裝飾器的細節:ios
裝飾器是一種特殊的聲明,可附加在類、方法、訪問器、屬性、參數聲明上。git
裝飾器使用 @expression
的形式,其中 expression
必須可以演算爲在運行時調用的函數,其中包括裝飾聲明信息。程序員
它起到了以聲明式方法將元信息添加至已有代碼的做用。github
裝飾器類型及其執行優先級爲typescript
注意,若是裝飾器應用於類構造函數的參數,那麼不一樣裝飾器的優先級爲:1. 參數裝飾器,2. 方法裝飾器,3. 訪問器或參數裝飾器,4. 構造器參數裝飾器,5. 類裝飾器。express
// 這是一個裝飾器工廠——有助於將用戶參數傳給裝飾器聲明
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(): evaluated
// g(): evaluated
// g(): called
// f(): called
複製代碼
咱們看到,上面的代碼中,f
和 g
返回了另外一個函數(裝飾器函數)。f
和 g
稱爲裝飾器工廠。macos
裝飾器工廠 幫助用戶傳遞可供裝飾器利用的參數。
咱們還能夠看到,演算順序爲由頂向下,執行順序爲由底向上。
declare type ClassDecorator =
<TFunction extends Function>(target: TFunction) => TFunction | void;
declare type PropertyDecorator =
(target: Object, propertyKey: string | symbol) => void;
declare type MethodDecorator = <T>(
target: Object, propertyKey: string | symbol,
descriptor: TypedPropertyDescriptor<T>) =>
TypedPropertyDescriptor<T> | void;
複製代碼
從上面的簽名中,咱們能夠看到方法裝飾器函數有三個參數:
Employee.prototype
Object.getOwnPropertyDescriptor(Employee.prototype, propertyKey)
export function logMethod( target: Object, propertyName: string, propertyDescriptor: PropertyDescriptor): PropertyDescriptor {
// target === Employee.prototype
// propertyName === "greet"
// propertyDesciptor === Object.getOwnPropertyDescriptor(Employee.prototype, "greet")
const method = propertyDesciptor.value;
propertyDesciptor.value = function (...args: any[]) {
// 將 greet 的參數列表轉換爲字符串
const params = args.map(a => JSON.stringify(a)).join();
// 調用 greet() 並獲取其返回值
const result = method.apply(this, args);
// 轉換結尾爲字符串
const r = JSON.stringify(result);
// 在終端顯示函數調用細節
console.log(`Call: ${propertyName}(${params}) => ${r}`);
// 返回調用函數的結果
return result;
}
return propertyDesciptor;
};
class Employee {
constructor(private firstName: string, private lastName: string ) {}
@logMethod
greet(message: string): string {
return `${this.firstName} ${this.lastName} says: ${message}`;
}
}
const emp = new Employee('Mohan Ram', 'Ratnakumar');
emp.greet('hello');
複製代碼
上面的代碼應該算是自解釋的——讓咱們看看編譯後的 JavaScript 是什麼樣的。
"use strict";
var __decorate = (this && this.__decorate) ||
function (decorators, target, key, desc) {
// 函數參數長度
var c = arguments.length
/** * 處理結果 * 若是僅僅傳入了裝飾器數組和目標,那麼應該是個類裝飾器。 * 不然,若是描述符(第 4 個參數)爲 null,就根據已知值準備屬性描述符, * 反之則使用同一描述符。 */
var r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc;
// 聲明存儲裝飾器的變量
var d;
// 若是原生反射可用,使用原生反射觸發裝飾器
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") {
r = Reflect.decorate(decorators, target, key, desc)
}
else {
// 自右向左迭代裝飾器
for (var i = decorators.length - 1; i >= 0; i--) {
// 若是裝飾器合法,將其賦值給 d
if (d = decorators[i]) {
/** * 若是僅僅傳入了裝飾器數組和目標,那麼應該是類裝飾器, * 傳入目標調用裝飾器。 * 不然,若是 4 個參數俱全,那麼應該是方法裝飾器, * 據此進行調用。 * 反之則使用同一描述符。 * 若是傳入了 3 個參數,那麼應該是屬性裝飾器,可進行相應的調用。 * 若是以上條件皆不知足,返回處理的結果。 */
r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r
}
}
};
/** * 因爲只有方法裝飾器須要根據應用裝飾器的結果修正其屬性, * 因此最後返回處理好的 r */
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var Employee = /** @class */ (function () {
function Employee(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
Employee.prototype.greet = function (message) {
return this.firstName + " " + this.lastName + " says: " + message;
};
// typescript 調用 `__decorate` 輔助函數,
// 以便在對象原型上應用裝飾器
__decorate([
logMethod
], Employee.prototype, "greet");
return Employee;
}());
var emp = new Employee('Mohan Ram', 'Ratnakumar');
emp.greet('hello');
複製代碼
讓咱們開始分析 Employee 函數——構造器初始化 name
參數和 greet
方法,將其加入原型。
__decorate([logMethod], Employee.prototype, "greet");
複製代碼
這是 TypeScript 自動生成的通用方法,它根據裝飾器類型和相應參數處理裝飾器函數調用。
該函數有助於內省方法調用,併爲開發者鋪平了處理相似日誌、記憶化、應用配置等橫切關注點的道路。
在這個例子中,咱們僅僅打印了函數調用及其參數、響應。
注意,閱讀 __decorate
方法中的詳細註釋能夠理解其內部機制。
屬性裝飾器函數有兩個參數:
Employee.prototype
function logParameter(target: Object, propertyName: string) {
// 屬性值
let _val = this[propertyName];
// 屬性讀取訪問器
const getter = () => {
console.log(`Get: ${propertyName} => ${_val}`);
return _val;
};
// 屬性寫入訪問器
const setter = newVal => {
console.log(`Set: ${propertyName} => ${newVal}`);
_val = newVal;
};
// 刪除屬性
if (delete this[propertyName]) {
// 建立新屬性及其讀取訪問器、寫入訪問器
Object.defineProperty(target, propertyName, {
get: getter,
set: setter,
enumerable: true,
configurable: true
});
}
}
class Employee {
@logParameter
name: string;
}
const emp = new Employee();
emp.name = 'Mohan Ram';
console.log(emp.name);
// Set: name => Mohan Ram
// Get: name => Mohan Ram
// Mohan Ram
複製代碼
上面的代碼中,咱們在裝飾器中內省屬性的可訪問性。下面是編譯後的代碼。
var Employee = /** @class */ (function () {
function Employee() {
}
__decorate([
logParameter
], Employee.prototype, "name");
return Employee;
}());
var emp = new Employee();
emp.name = 'Mohan Ram'; // Set: name => Mohan Ram
console.log(emp.name); // Get: name => Mohan Ram
複製代碼
參數裝飾器函數有三個參數:
Employee.prototype
function logParameter(target: Object, propertyName: string, index: number) {
// 爲相應方法生成元數據鍵,以儲存被裝飾的參數的位置
const metadataKey = `log_${propertyName}_parameters`;
if (Array.isArray(target[metadataKey])) {
target[metadataKey].push(index);
}
else {
target[metadataKey] = [index];
}
}
class Employee {
greet(@logParameter message: string): string {
return `hello ${message}`;
}
}
const emp = new Employee();
emp.greet('hello');
複製代碼
在上面的代碼中,咱們收集了全部被裝飾的方法參數的索引或位置,做爲元數據加入對象的原型。下面是編譯後的代碼。
// 返回接受參數索引和裝飾器的函數
var __param = (this && this.__param) || function (paramIndex, decorator) {
// 該函數返回裝飾器
return function (target, key) { decorator(target, key, paramIndex); }
};
var Employee = /** @class */ (function () {
function Employee() {}
Employee.prototype.greet = function (message) {
return "hello " + message;
};
__decorate([
__param(0, logParameter)
], Employee.prototype, "greet");
return Employee;
}());
var emp = new Employee();
emp.greet('hello');
複製代碼
相似以前見過的 __decorate
函數,__param
函數返回一個封裝參數裝飾器的裝飾器。
如咱們所見,調用參數裝飾器時,會忽略其返回值。這意味着,調用 __param
函數時,其返回值不會用來覆蓋參數值。
這就是參數裝飾器不返回的緣由所在。
訪問器不過是類聲明中屬性的讀取訪問器和寫入訪問器。
訪問器裝飾器應用於訪問器的屬性描述符,可用於觀測、修改、替換訪問器的定義。
function enumerable(value: boolean) {
return function ( target: any, propertyKey: string, descriptor: PropertyDescriptor) {
console.log('decorator - sets the enumeration part of the accessor');
descriptor.enumerable = value;
};
}
class Employee {
private _salary: number;
private _name: string;
@enumerable(false)
get salary() { return `Rs. ${this._salary}`; }
set salary(salary: any) { this._salary = +salary; }
@enumerable(true)
get name() {
return `Sir/Madam, ${this._name}`;
}
set name(name: string) {
this._name = name;
}
}
const emp = new Employee();
emp.salary = 1000;
for (let prop in emp) {
console.log(`enumerable property = ${prop}`);
}
// salary 屬性不在清單上,由於咱們將其設爲假
// output:
// decorator - sets the enumeration part of the accessor
// decorator - sets the enumeration part of the accessor
// enumerable property = _salary
// enumerable property = name
複製代碼
上面的例子中,咱們定義了兩個訪問器 name
和 salary
,並經過裝飾器設置是否將其列入清單,據此決定對象的行爲。name
將列入清單,而 salary
不會。
注意:TypeScript 不容許同時裝飾單一成員的 get
和 set
訪問器。相反,全部成員的裝飾器都必須應用於首個指定的訪問器(根據文檔順序)。這是由於裝飾器應用於屬性描述符,屬性描述符結合了 get
和 set
訪問器,而不是分別應用於每項聲明。
下面是編譯的代碼。
function enumerable(value) {
return function (target, propertyKey, descriptor) {
console.log('decorator - sets the enumeration part of the accessor');
descriptor.enumerable = value;
};
}
var Employee = /** @class */ (function () {
function Employee() {
}
Object.defineProperty(Employee.prototype, "salary", {
get: function () { return "Rs. " + this._salary; },
set: function (salary) { this._salary = +salary; },
enumerable: true,
configurable: true
});
Object.defineProperty(Employee.prototype, "name", {
get: function () {
return "Sir/Madam, " + this._name;
},
set: function (name) {
this._name = name;
},
enumerable: true,
configurable: true
});
__decorate([
enumerable(false)
], Employee.prototype, "salary", null);
__decorate([
enumerable(true)
], Employee.prototype, "name", null);
return Employee;
}());
var emp = new Employee();
emp.salary = 1000;
for (var prop in emp) {
console.log("enumerable property = " + prop);
}
複製代碼
類裝飾器應用於類的構造器,可用於觀測、修改、替換類定義。
export function logClass(target: Function) {
// 保存一份原構造器的引用
const original = target;
// 生成類的實例的輔助函數
function construct(constructor, args) {
const c: any = function () {
return constructor.apply(this, args);
}
c.prototype = constructor.prototype;
return new c();
}
// 新構造器行爲
const f: any = function (...args) {
console.log(`New: ${original['name']} is created`);
return construct(original, args);
}
// 複製 prototype 屬性,保持 intanceof 操做符可用
f.prototype = original.prototype;
// 返回新構造器(將覆蓋原構造器)
return f;
}
@logClass
class Employee {}
let emp = new Employee();
console.log('emp instanceof Employee');
console.log(emp instanceof Employee); // true
複製代碼
上面的裝飾器聲明瞭一個名爲 original
的變量,將其值設爲被裝飾的類構造器。
接着聲明瞭名爲 construct
的輔助函數。該函數用於建立類的實例。
咱們接下來建立了一個名爲 f
的變量,該變量將用做新構造器。該函數調用原構造器,同時在控制檯打印實例化的類名。這正是咱們給原構造器加入額外行爲的地方。
原構造器的原型複製到 f
,以確保建立一個 Employee 新實例的時候,instanceof
操做符的效果符合預期。
新構造器一旦就緒,咱們便返回它,以完成類構造器的實現。
新構造器就緒以後,每次建立實例時會在控制檯打印類名。
編譯後的代碼以下。
var Employee = /** @class */ (function () {
function Employee() {
}
Employee = __decorate([
logClass
], Employee);
return Employee;
}());
var emp = new Employee();
console.log('emp instanceof Employee');
console.log(emp instanceof Employee);
複製代碼
在編譯後的代碼中,咱們注意到兩處不一樣:
__decorate
的參數有兩個,裝飾器數組和構造器函數。__decorate
的返回值以覆蓋原構造器。這正是類裝飾器必須返回一個構造函數的緣由所在。
因爲每種裝飾器都有它自身的調用簽名,咱們能夠使用裝飾器工廠來泛化裝飾器調用。
import { logClass } from './class-decorator';
import { logMethod } from './method-decorator';
import { logProperty } from './property-decorator';
import { logParameter } from './parameter-decorator';
// 裝飾器工廠,根據傳入的參數調用相應的裝飾器
export function log(...args) {
switch (args.length) {
case 3: // 多是方法裝飾器或參數裝飾器
// 若是第三個參數是數字,那麼它是索引,因此這是參數裝飾器
if typeof args[2] === "number") {
return logParameter.apply(this, args);
}
return logMethod.apply(this, args);
case 2: // 屬性裝飾器
return logProperty.apply(this, args);
case 1: // 類裝飾器
return logClass.apply(this, args);
default: // 參數數目不合法
throw new Error('Not a valid decorator');
}
}
@log
class Employee {
@log
private name: string;
constructor(name: string) {
this.name = name;
}
@log
greet(@log message: string): string {
return `${this.name} says: ${message}`;
}
}
複製代碼
元信息反射 API (例如 Reflect
)可以用來以標準方式組織元信息。
「反射」的意思是代碼能夠偵測同一系統中的其餘代碼(或其自身)。
反射在組合/依賴注入、運行時類型斷言、測試等使用場景下頗有用。
import "reflect-metadata";
// 參數裝飾器使用反射 api 存儲被裝飾參數的索引
export function logParameter(target: Object, propertyName: string, index: number) {
// 獲取目標對象的元信息
const indices = Reflect.getMetadata(`log_${propertyName}_parameters`, target, propertyName) || [];
indices.push(index);
// 定義目標對象的元信息
Reflect.defineMetadata(`log_${propertyName}_parameters`, indices, target, propertyName);
}
// 屬性裝飾器使用反射 api 獲取屬性的運行時類型
export function logProperty(target: Object, propertyName: string): void {
// 獲取對象屬性的設計類型
var t = Reflect.getMetadata("design:type", target, propertyName);
console.log(`${propertyName} type: ${t.name}`); // name type: String
}
class Employee {
@logProperty
private name: string;
constructor(name: string) {
this.name = name;
}
greet(@logParameter message: string): string {
return `${this.name} says: ${message}`;
}
}
複製代碼
上面的代碼用到了 reflect-metadata 這個庫。其中,咱們使用了反射元信息的設計鍵(例如:design:type
)。目前只有三個:
design:type
。design:paramtypes
。design:returntype
。有了反射,咱們就可以在運行時獲得如下信息:
我把文中全部代碼示例都放到了 mohanramphp/typescript-decorators 這個 Git 倉庫中。謝謝閱讀!
題圖:Alex Loup