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'

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值