从C#到TypeScript - 装饰器

从C#到TypeScript - 装饰器

在C#里面如果想要不直接修改类或方法,但给类或方法添加一些额外的信息或功能,可以想到用Attribute,这是一个十分方便的功能装饰器。 用TypeScript同样也可以利用装饰器来给类、函数、属性以及参数添加附加功能,装饰器是ES7的一个提案,在TypeScript里已经有实现可用,不过需要在tsconfig.json里启用experimentalDecorators

"compilerOptions": {
    ..., // other options
    "experimentalDecorators": true
}

装饰器介绍

TypeScript中装饰器可以应用到类、方法、属性及函数参数上,而且可以同时应用多个。 装饰器的写法是@name()()可以不要,也可以在里面写一些参数。

@Testable
@Log('controller')
class Controller{

    @GET
    getContent(@QueryParam arg: string): string{
        return '';
    }
}

装饰器的实现

装饰器根据实现可以分两种: 一种是不带括号,和属性一样,如@Testable

function Testable(target: Function) { // 类、方法、属性、方法参数的参数各不相同
    //这里可以记录一些信息到target,或者针对target做一些处理,如seal
}

另外一种是带括号的,和函数一样,如@Log('controller'),实现函数里的参数就是括号里的参数,而且需要返回一个function

function Log(name: string) { // name就是传进来的参数'controller'
    return function(target: Function) { // 类、方法、属性、方法参数的参数各不相同
        // 这里可以根据name和target来做一些处理
    }
}

类装饰器

上面的(target: Function)其实就是类的装饰器参数,指向的是类的构造函数,如果想给类加一个简单的seal功能,可以这样做:

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

@sealed
class Test{
    
}

Test.prototype.test = ''; // 运行时出错,不能添加

上面的sealed就是类的装饰器,target指构造函数,类装饰器就这么一个参数。

方法装饰器

方法装饰器的使用方法和类装饰器类似,只是参数不一样,方法装饰器有三个参数:

  1. 如果装饰的是静态方法,则是类的构造函数,如果是实例方法则是类的原型。
  2. 方法的名字。
  3. 方法的PropertyDescriptorPropertyDescriptor即属性描述符,有 configurable 是否可以配置,如动态添加删除函数属性之类 writable 是否可写,可以用来设置只读属性 enumerable 是否可枚举,即是否能在for...in中能枚举到 value 对象或属性的值

有了这些参数就可以很好的给方法添加一些功能,比如下面实现类型WebApi里的Get的路由:

const Router = Symbol(); // 唯一key,用来存装饰器的信息

function GET(path?: string) { // GET带了个可选参数
    return (target: any, name: string) => setMethodDecorator(target, name, 'GET', path);
}

//把method和path存起来,路由查找的时候就可以用了
function setMethodDecorator(target: any, name: string, method: string, path?: string){
    target[Router] = target[Router] || {};
    target[Router][name] = target[Router][name] || {};
    target[Router][name].method = method;
    target[Router][name].path = path;
}

// 通过PropertyDescriptor来设置enumerable
function Enumerable(enumerable: boolean) { 
    return (target: any, name: string, descriptor: PropertyDescriptor) => {
        descriptor.enumerable = enumerable;
    };
}

class Controller{

    @GET
    @Enumerable(true)
    getContent(arg: string): string{
        return '';
    }
}

参数装饰器

方法参数同样可以有装饰器,同样有三个参数,前两个参数和方法的一致,最后一个参数是所装饰的参数的位置。 能过参数装饰器可以给方法动态的检查或设置参数值,下面是检查参数是否为空,为空则抛出异常。

const CheckNullKey = Symbol();
const Router = Symbol();

// 把CheckNull装饰的参数存起来
function CheckNull(target: any, name: string, index: number) {
    target[Router] = target[Router] || {};
    target[Router][name] = target[Router][name] || {};
    target[Router][name].params = target[Router][name].params || [];
    target[Router][name].params[index] = CheckNullKey;
}

// 找出CheckNull的参数,并检查参数值,为空则抛异常,否则继续执行方法
function Check(target: any, name: string, descriptor: PropertyDescriptor) {
    let method = descriptor.value;
    descriptor.value = function () {
        let params = target[Router][name].params;
        if (params) {
            for (let index = 0; index < params.length; index++) {
                if (params[index] == CheckNullKey &&  // 找到CheckNull的参数并抛异常
                    (arguments[index] === undefined || arguments[index] === null)) {
                    throw new Error("Missing required argument.");
                }
            }
        }

        return method.apply(this, arguments);
    }
}

class Controller{

    @Check
    getContent(@CheckNull id: string): string{
        console.info(id);
        return id;
    }
}

new Controller().getContent(null); // error : Missing required argument.

属性装饰器

用法同上,参数只有两个,和类装饰器的前两个一样,常用来标识属性的特性。

function Column(target: any, name: string) {
    //把name存起来,这个column仅仅是标识出来对应数据库中的列,常用在ORM框架中
}

class Table{

    @Column  
    name: string;
}

另外还有属性访问器的装饰器,和方法基本一样,同样的三个参数,不过同个属性的getset只能有一个有,而且必须是先声明的那个。

class User {
    private _name: string;

    @Enumerable(true)
    get name(){
        return this._name;
    }

    set name(value: string) {
        this._name = value;
    }
}

多个装饰器的执行顺序

一个声明可以添加多个装饰器,所以会有个执行先后顺序。 首先从上到下执行装饰器函数,然后再从下往上应用带括号的装饰器返回的函数。

function Test1(){
    console.info('eval test1');
    return function(target: any, name: string, descriptor: PropertyDescriptor){
        console.info('apply test1');
    }
}

function Test2(){
    console.info('eval test2');
    return function(target: any, name: string, descriptor: PropertyDescriptor){
        console.info('apply test2');
    }
}

class User1{

    @test1()
    @Test2()
    getName(){

    }
}

结果是:

eval test1
eval test2
apply test2
apply test1

总之,装饰器等于引入了天然的装饰模式,给类,方法等添加额外功能。不过装饰器目前还不算太稳定,但是由于确实方便,已经有成熟项目在使用了。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏HTML5学堂

JS 计时器参数剖析与真题

HTML5学堂-码匠:计时器的第一个参数,包含几种不同的书写方法,可以是函数名,匿名函数,JS代码字符串,还有一些面试题当中会出现“函数调用”的书写方式。 那么...

2714
来自专栏xingoo, 一个梦想做发明家的程序员

【AngularJS】—— 4 表达式

前面了解了AngularJS的基本用法,这里就跟着PDF一起学习下表达式的相关内容。   在AngularJS中的表达式,与js中并不完全相同。   首先...

1975
来自专栏偏前端工程师的驿站

Velocity魔法堂系列二:VTL语法详解

一、前言                               Velocity作为历史悠久的模板引擎不单单可以替代JSP作为Java Web的服务端网页...

1955
来自专栏Nian糕的私人厨房

JavaScript 常见面试题分析(二)

④ call() 方法 apply() 方法 bind() 方法 (this 指向第一个参数)

793
来自专栏LuckQI

Redis初识~List命令

862
来自专栏debugeeker的专栏

《coredump问题原理探究》Linux x86版4.5节函数的逆向之coredump例子

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/xuzhina/article/detai...

402
来自专栏HTML5学堂

轻松但深入的学习闭包原理 —— 曾让几乎所有JS新手痛恨的知识

HTML5学堂-码匠:这或许是你看过的,最浅显易懂的一篇关于闭包原理的讲解! 闭包的官方定义 官方定义:闭包是一个拥有许多变量和绑定了这些变量的环境的表达式(通...

3346
来自专栏xingoo, 一个梦想做发明家的程序员

Oozie分布式工作流——EL表达式

oozie支持使用EL(expression language)表达式。 基本的EL常量 KB MB GB TB PB 基本EL函数 string fir...

2178
来自专栏编码小白

tomcat源码解读三(1) tomcat的jmx管理

    JMX即Java 管理扩展(Java Management Extensions,JMX)用来管理检测 Java 程序(同时 JMX 也在 J2EE 1...

3548
来自专栏SpringBoot

MongoDB基本数据类型

643

扫码关注云+社区