準備工作
npm init -y
npm i --save-dev gulp gulp-typescript typescript
npm i --save reflect-metadata
gulpfile.js
const gulp = require('gulp')
const tsc = require('gulp-typescript')
const typescript = require('typescript')
const tsProject = tsc.createProject({
removeComments: false,
noImplicitAny: false,
target: 'es5',
module: 'commonjs',
declarationFiles: false,
emitDecoratorMetadata: true,
typescript: typescript
})
gulp.task('buil-source', () => {
return gulp.src(`${__dirname}/file.ts`)
.pipe(tsc(tsProject))
.js.pipe(gulp.dest(`${__dirname}/`))
})
註解和裝飾器
註解是一種爲類聲明添加元數據的方法。然後,元數據就可以被諸如依賴注入容器這樣的工具所使用。
裝飾器,用來在代碼設計時註釋和修改類和類的屬性。已成爲ES7標準的特性。
class Person {
public name: string
public surname: string
constructor(name: string, surname: string) {
this.name = name
this.surname = surname
}
public saySomething(something: string): string {
return `${this.name} ${surname} says: ${something}`
}
}
可供使用的裝飾器一共有4種,分別用來裝飾:類、屬性、方法和參數
類裝飾器
類裝飾器是指接受一個類構造函數作爲參數的函數,並且返回undefined、參數中提供的構造函數或一個新的構造函數。返回undefined等同於返回參數中提供的構造函數。
類裝飾器用來修改類的構造函數。如果裝飾器函數返回undefined,那麼類仍然使用原來的構造函數。如果裝飾器有返回值,那麼返回值會被用來覆蓋原來的構造函數。
創建一個裝飾器
function logClass(target: any){
//...
}
使用裝飾器裝飾一個類
@logClass
class Person {
public name: string
public surname: string
//...
}
編譯結果
var Person = (function() {
function Person(name, surname){
this.name = name
this.surname = surname
}
Person.prototype.saySomething = function(something) {
return `${this.name} ${surname} says: ${something}`
}
Person = _decorate([
logClass
], Person)
return Person
})()
裝飾器用來爲元素添加一些額外的邏輯或元數據。當我們想拓展一個函數的功能時,需要往原函數上包一個新函數,新函數裏有額外的邏輯,且能執行原函數裏的方法。
function logClass(target: any){
//保存原構造函數的引用
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}`)
return construct(original, args)
}
// 複製原型,使instanceof操作能正常使用
f.prototype = original.prototype
//返回新的構造函數(將會覆蓋原構造函數)
return f
}
哎裝飾了類的構造函數後,會有
var me = new Person('Remo', 'Jansen') // 輸出"New: Person"
方法裝飾器
方法裝飾器和類裝飾器十分相似,但是他用來覆蓋類的方法。
//...
@logMethod
public saySomething(something: string) : string {
return `${this.name} ${surname} says: ${something}`
}
方法裝飾器被調用時,帶有以下參數:
- 包含了被裝飾方法的類的原型。即Person.prototype
- 被裝飾方法的名字,即saySomething
- 被裝飾方法的屬性描述對象,即Object
function logMethod(target: any, key: string, descriptor: any) {
// 保存原方法的引用
const originalMethod = descriptor.value
// 編輯descriptor參數的value屬性
descriptor.value = function(...args: any[]){
// 將方法參數轉換爲字符串
const a = args.map(a => JSON.stringify(a)).join()
// 執行方法得到其返回值
const result = originalMethod.apply(this, args)
// 將返回值轉化爲字符串
const r = JSON.stringify(result)
// 將函數的調用細節打印在控制檯中
console.log(`Call:${key}(${a}) => ${r}`)
// 返回方法的調用結果
return result
}
// 返回編輯後的屬性描述對象
return descriptor
}
運行結果
const me = new Person('Remo', 'Jansen')
me.saySomething('hello!')
//Call: saySomething('hello') => 'Remo Jansen says: hello!'
屬性裝飾器
屬性裝飾器和方法裝飾器十分相似。主要區別在於,一個屬性裝飾器沒有返回值且沒有第三個參數(屬性描述對象)
class Person {
@logProperty
public name: string
}
使用一個新屬性來替代原來的屬性,新屬性會表現得與原屬性一致,除了在更改時會將改變的值打印在控制檯中
function logProperty(target: any, key: string) {
// 屬性值
const _val = this[key]
// 屬性的getter
const getter = function() {
console.log(`Get:${key} => ${_val}`)
return _val
}
// 屬性的setter
const setter = function(newVal) {
console.log(`Set:${} => ${newVal}`)
_val = newVal
}
// 刪除屬性,在嚴格模式下,如果對象是不可配置的,
// 刪除操作將會拋出一個錯誤。在非嚴格模式下,則會返回false
if(delete this[key]) {
Object.defineProperty(target, key, {
get: getter,
set: setter,
enumerable: true,
configurable: true
})
}
}
使用屬性裝飾器
const me = new Person('Remo', 'Jansen')
// Set: name => Remo
me.name = 'Remo H.'
// Set: name => Remo H.
const n = me.name
// Get: name Remo H.
參數裝飾器
參數裝飾器函數是一個接收三個參數的函數:一個包含了被裝飾參數的方法的對象、方法的名字(或undefined)和參數在參數列表中的索引。裝飾器的返回值將被忽略。
public saySomething(@addMetadata something: string) : string {
return `${this.name} ${this.surname} says: ${something}`
}
參數屬性沒有返回值,意味不能覆蓋整個包含被修飾參數的方法
function addMetadata(target: any, key: string, index: number) {
const metadataKey = `_log_${key}_parameters`
if(Array.isArray(target[metadataKey])) {
target[metadataKey].push(index)
} else {
target[metadataKey] = [index]
}
}
單獨的參數裝飾器並不是很有用,他需要和方法裝飾器結合,參數裝飾器用來添加元數據,然後通過方法裝飾器來讀取它。
@readMetadata
public saySomething(@addMetadata something : string) : string {
return `${this.name} ${surname} says: ${something}`
}
打印被裝飾的參數
function readMetadata(target: any, key: string, descriptor: any) {
const originalMethod = descriptor.value
descriptor.value = function(...args: any[]){
const metadataKey = `_log_${key}_parameters`
const indices = target[metadataKey]
if(Array.isArray(indices)) {
for(let i = 0; i < args.length; i++) {
if (indices.indexOf(i) !== -1) {
const arg = args[i]
const argStr = JSON.stringify(arg) || arg.toString()
console.log(`${key} arg[${i}]:${argStr}`)
}
}
const result = originalMethod.apply(this, args)
return result
}
}
return descriptor
}
執行
const person = new Person('Remo', 'Jansen')
person.saySomething('hello')
// saySomething arg[0]: 'hello!'
裝飾器工廠
裝飾器工廠是一個接收任意數量參數的函數;並且必須返回上述的任意一種裝飾器。
可以使用裝飾器工廠來使裝飾器更容易被使用
@logClass
class Person {
@logProperty
public name: string
@logMethod
public saySomething(@logParameter something: string): string {
return `${this.name} ${surname} says: ${something}`
}
}
通用裝飾器
@log
class Person {
@log
public name: string
@log
public saySomething(@log something: string): string {
return `${this.name} ${surname} says: ${something}`
}
}
實現
function log(...args: any[]) {
switch(args.length) {
case 1:
return logClass.apply(this, args)
case 2:
// 由於屬性裝飾器沒有返回值
// 所以使用break取代return
logProperty.apply(this, args)
case 3:
if(typeof args[2] === 'number') {
logParameter.apply(this, args)
}
return logMethod.apply(this, args)
default:
throw new Error('Decorators are not valid here!')
}
}
帶有參數的裝飾器
可以使用一種特殊的裝飾器工廠來配置裝飾器的行爲。
@logClass('option')
class Person {
//...
}
爲了給裝飾器傳遞參數,需要使用一個函數來包裹裝飾器。這個包裹函數接收參數並返回一個裝飾器
function logClass(option: string) {
return function(target: any) {
// 類裝飾器的邏輯
// 可以訪問到裝飾器的參數
console.log(target, option)
}
}
反射元數據API
不久後,TS團隊決定使用反射元信息API來替代這些保留裝飾器。他的思想與使用保留裝飾器十分相似,但使用了反射元信息API代替保留裝飾器,來獲取元信息。TS文檔中定義了三種保留元數據鍵:
- 類型元數據使用元數據鍵 design:type
- 參數類型元數據使用元數據鍵 design:paramtypes
- 返回值元數據使用元數據鍵 design: returntype
使用,引用並導入一個包
///<reference path='./node_modules/reflect-metadata/reflect-metadata.d.ts'/>
import 'reflect-metadata'
爲了測試,創建一個類,而在運行時,去獲取類的一個屬性的類型。
class Demo {
@logType
public attr1: string
}
調用Reflect.getMetadata()方法並傳入design:type鍵,而不是使用保留裝飾器@type來獲取屬性類型。
function logType(target: any, key: string) {
var t = Reflect.getMetadata('design:type', target, key)
console.log(`${key} type: ${t.name}`)
}
輸出
'attr1 type: String'