TypeScript 详解之 TypeScript 装饰器(旧语法)

experimentalDecorators 编译选项

使用装饰器的旧语法,需要打开--experimentalDecorators编译选项。

$ tsc --target ES5 --experimentalDecorators

此外,还有另外一个编译选项--emitDecoratorMetadata,用来产生一些装饰器的元数据,供其他工具或某些模块(比如 reflect-metadata )使用。

这两个编译选项可以在命令行设置,也可以在tsconfig.json文件里面进行设置。

{
  "compilerOptions": {
    "target": "ES6",
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}

装饰器的种类

按照所装饰的不同对象,装饰器可以分成五类。

  • 类装饰器(Class Decorators):用于类。
  • 属性装饰器(Property Decorators):用于属性。
  • 方法装饰器(Method Decorators):用于方法。
  • 存取器装饰器(Accessor Decorators):用于类的 set 或 get 方法。
  • 参数装饰器(Parameter Decorators):用于方法的参数。

下面是这五种装饰器一起使用的一个示例。

@ClassDecorator() // (A)
class A {

  @PropertyDecorator() // (B)
  name: string;

  @MethodDecorator() //(C)
  fly(
    @ParameterDecorator() // (D)
    meters: number
  ) {
    // code
  }

  @AccessorDecorator() // (E)
  get egg() {
    // code
  }
  set egg(e) {
    // code
  }
}

上面示例中,A 是类装饰器,B 是属性装饰器,C 是方法装饰器,D 是参数装饰器,E 是存取器装饰器。

注意,构造方法没有方法装饰器,只有参数装饰器。类装饰器其实就是在装饰构造方法。

另外,装饰器只能用于类,要么应用于类的整体,要么应用于类的内部成员,不能用于独立的函数。

function Decorator() {
  console.log('In Decorator');
}

@Decorator // 报错
function decorated() {
  console.log('in decorated');
}

上面示例中,装饰器用于一个普通函数,这是无效的,结果报错。

类装饰器

类装饰器应用于类(class),但实际上是应用于类的构造方法。

类装饰器有唯一参数,就是构造方法,可以在装饰器内部,对构造方法进行各种改造。如果类装饰器有返回值,就会替换掉原来的构造方法。

类装饰器的类型定义如下。

type ClassDecorator = <TFunction extends Function>
  (target: TFunction) => TFunction | void;

上面定义中,类型参数TFunction必须是函数,实际上就是构造方法。类装饰器的返回值,要么是返回处理后的原始构造方法,要么返回一个新的构造方法。

下面就是一个示例。

function f(target:any) {
  console.log('apply decorator')
  return target;
}

@f
class A {}
// 输出:apply decorator

上面示例中,使用了装饰器@f,因此类A的构造方法会自动传入f

A不需要新建实例,装饰器也会执行。装饰器会在代码加载阶段执行,而不是在运行时执行,而且只会执行一次。

由于 TypeScript 存在编译阶段,所以装饰器对类的行为的改变,实际上发生在编译阶段。这意味着,TypeScript 装饰器能在编译阶段运行代码,也就是说,它本质就是编译时执行的函数。

下面再看一个示例。

@sealed
class BugReport {
  type = "report";
  title: string;

  constructor(t:string) {
    this.title = t;
  }
}

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

上面示例中,装饰器@sealed()会锁定BugReport这个类,使得它无法新增或删除静态成员和实例成员。

如果除了构造方法,类装饰器还需要其他参数,可以采取“工厂模式”,即把装饰器写在一个函数里面,该函数可以接受其他参数,执行后返回装饰器。但是,这样就需要调用装饰器的时候,先执行一次工厂函数。

function factory(info:string) {
  console.log('received: ', info);
  return function (target:any) {
    console.log('apply decorator');
    return target;
  }
}

@factory('log something')
class A {}

上面示例中,函数factory()的返回值才是装饰器,所以加载装饰器的时候,要先执行一次@factory('log something'),才能得到装饰器。这样做的好处是,可以加入额外的参数,本例是参数info

总之,@后面要么是一个函数名,要么是函数表达式,甚至可以写出下面这样的代码。

@((constructor: Function) => {
  console.log('log something');
})
class InlineDecoratorExample {
  // ...
}

上面示例中,@后面是一个箭头函数,这也是合法的。

类装饰器可以没有返回值,如果有返回值,就会替代所装饰的类的构造函数。由于 JavaScript 的类等同于构造函数的语法糖,所以装饰器通常返回一个新的类,对原有的类进行修改或扩展。

function decorator(target:any) {
  return class extends target {
    value = 123;  
  };
}

@decorator
class Foo {
  value = 456;
}

const foo = new Foo();
console.log(foo.value); // 123

上面示例中,装饰器decorator返回一个新的类,替代了原来的类。

上例的装饰器参数target类型是any,可以改成构造方法,这样就更准确了。

type Constructor = {
  new(...args: any[]): {}
};

function decorator<T extends Constructor> (
  target: T
) {
  return class extends target {
    value = 123;  
  };
}

这时,装饰器的行为就是下面这样。

@decorator
class A {}

// 等同于
class A {}
A = decorator(A) || A;

上面代码中,装饰器要么返回一个新的类A,要么不返回任何值,A保持装饰器处理后的状态。

方法装饰器

方法装饰器用来装饰类的方法,它的类型定义如下。

type MethodDecorator = <T>(
  target: Object,
  propertyKey: string|symbol,
  descriptor: TypedPropertyDescriptor<T>
) => TypedPropertyDescriptor<T> | void;

方法装饰器一共可以接受三个参数。

  • target:(对于类的静态方法)类的构造函数,或者(对于类的实例方法)类的原型。
  • propertyKey:所装饰方法的方法名,类型为string|symbol
  • descriptor:所装饰方法的描述对象。

方法装饰器的返回值(如果有的话),就是修改后的该方法的描述对象,可以覆盖原始方法的描述对象。

下面是一个示例。

function enumerable(value: boolean) {
  return function (
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    descriptor.enumerable = value;
  };
}

class Greeter {
  greeting: string;

  constructor(message:string) {
    this.greeting = message;
  }

  @enumerable(false)
  greet() {
    return 'Hello, ' + this.greeting;
  }
}

上面示例中,方法装饰器@enumerable()装饰 Greeter 类的greet()方法,作用是修改该方法的描述对象的可遍历性属性enumerable@enumerable(false)表示将该方法修改成不可遍历。

下面再看一个例子。

还有 65% 的精彩内容
©著作权归作者所有,转载或内容合作请联系作者
支付 ¥4.99 继续阅读
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 162,825评论 4 377
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,887评论 2 308
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 112,425评论 0 255
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,801评论 0 224
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 53,252评论 3 299
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 41,089评论 1 226
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,216评论 2 322
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 31,005评论 0 215
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,747评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,883评论 2 255
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,354评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,694评论 3 265
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,406评论 3 246
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,222评论 0 9
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,996评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,242评论 2 287
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 36,017评论 2 281

推荐阅读更多精彩内容