Learning TypeScript 0x7 裝飾器

準備工作

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'

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章