TypeScript学习笔记-3-枚举、函数、类、装饰器

512 阅读24分钟

文章内容主要是官网的概念(枚举函数装饰器),以及自己进行练习例子。

枚举

枚举是TypeScript非类型扩展中的一个。使用枚举可以定义一个命名常数的集合。TypeScript支持数字和字符串枚举。

使用enum关键字定义枚举:

enum A {
  M1,
  M2
}

数字枚举

enum A1 {
  M1, 
  M2,
  M3
}
console.log(A1.M1, A1.M2, A1.M3); // 打印:0 1 2

1.枚举默认从0开始,后续的成员自动递增,上例中A1.M1, A1.M2, A1.M3分别为0 1 2。枚举的名称和枚举成员的名称一般都是大写驼峰。

2.可以对枚举成员进行初始化:

enum A1 {
  M1, 
  M2 = 5,
  M3
}
console.log(A1.M1, A1.M2, A1.M3); // 打印:0 5 6

M2被初始化为5之后,后续的成员从5开始递增。M35 + 1

3.用枚举的名称来表示枚举的类型:

enum A1 {
  M1, 
  M2 = 5,
  M3
}

function a (x: A1): A1 {
  return x;
}

a(A1.M1);

xA1.M1,它的类型用A1表示。

数字枚举要么在第一个位置,要么在使用数字常量或者其他常量枚举成员初始化的数字枚举之后。

enum A2 {
  M1 = 1, // 数字枚举在第一个位置
  M2
}
enum A3 {
  M1 = 2, 
  M2 // 数字枚举在被数字常量2初始化的枚举成员A后面
}
enum A4 {
  M1, 
  M2 = M1,
  M3 // 数字枚举在被常量枚举成员初始化的成员B后面
}
enum A5 {
  M1 = '', 
  M2 // 报错:Enum member must have initializer.
}

字符串枚举

在字符串枚举中,每一个成员必需使用字符串字面量进行常量初始化。

enum A6 {
  M1 = 'A_A',
  M2 = 'B_B',
  M3 = 'C_C',
}

异构枚举

同时包含数字枚举和字符串枚举的枚举。官方建议不要使用异构枚举。

enum A7 {
  M1 = 'A_A',
  M2 = 1,
}

常量成员和计算成员

每一个枚举成员都有一个和它关联的常量值或计算值。

下面这些情况下,一个枚举成员被认为是常量:

1.是第一个枚举成员并且没有被初始化,这种情况下它被赋予值0

enum A8 {
  M1,
}

A8.M1被赋值为0

2.没有被初始化且前一个枚举成员是一个数字常量。在这种情况下,成员的值是前一个枚举成员的值加1

enum A9 {
  M1 = 5,
  M2,
}

A9.M2的值是A9.M1 + 1,即5 + 16

3.被一个常量枚举表达式初始化。

常量枚举表达式的特征是:

(1)一个字面量枚举表达式(基本上是字符串字面量或数字字面量)。

(2)对先前定义的常量枚举成员的引用(可以来自不同枚举)。

(3)带括号的常量枚举表达式。

(4)包含+-~的一元运算符常量枚举表达式。

(4)使用+-*/%<<>>>>>&|^二元运算符的常量枚举表达式。

enum A10 {
  M1 = 1,
  M2 = (1 + 1),
  M3 = -1,
  M4 = 1 * 2,
}

所有其他情况下的枚举成员都被认为是计算的。

联合枚举和枚举成员类型

字面量枚举成员是一个没有初始化值的常量枚举成员,或是被包含字符串字面量、数字字面量、或包含一元运算符的数字字面量初始化的常量枚举成员。

当枚举中的所有枚举成员都是字面量枚举成员的时候,一些特殊的语义出现了:

1.枚举成员也成了类型。

enum A11 {
  M1,
  M2,
  M3
}

interface A12 {
  m1: A11.M1;
}

const a1: A12 =  {
  m1: A11.M1,
};

这里m1的类型并不是确切的为0A11.M1的类型根据推断应该是number,因为我像下面这样定义了下没有报类型错误:

const a1: A12 =  {
  m1: 111,
};

111并不是A11.M1的值。

接下来看看字符串枚举:

enum A13 {
  M1 = 'A_A',
  M2 = 'B_B',
  M3 = 'C_C'
}

interface A14 {
  m1: A13.M1;
}

const a2: A14 =  {
  m1: A13.M1,
};

a2m1属性一定要为A13.M1,否则会报类型错误:

const a2: A14 =  {
  m1: 'A_A', // 报错:Type '"A_A"' is not assignable to type 'A13.M1'.
};

即使'A_A'就是A13.M1的值。

2.枚举类型成了所有枚举成员的并集。

enum A15 {
  M1,
  M2,
  M3
}

const a3: A15 = 0;
const a4: A15.M1 | A15.M2 | A15.M3 = a3;

运行时枚举

枚举是一个运行时真实存在的对象。

enum A16 {
  M1,
  M2
}

function a5 (x: { M1: number }): void {
  console.log(x.M1);
}

a5(A16); // 直接将枚举A16作为一个参数传入,打印:0

就像使用一个包含属性M1、M2的对象那样来使用枚举A16A16.M1。枚举中的属性是只读属性,不可以被修改。

编译时枚举

虽然枚举是一个在运行时真实存在的对象,但是使用keyof不能拿到枚举的键名,需要使用keyof typeof拿到所有枚举关键字的联合类型。

enum A17 {
  M1,
  M2,
  M3,
}

const a6: 'M1' | 'M2' | 'M3' = 'M1';
const a7: keyof typeof A17 = a6;

反向匹配

数字枚举成员有一个从枚举值到枚举名的反向匹配。注意字符串枚举没有反向匹配。

enum A18 {
  M1,
  M2,
  M3,
}

enum A19 {
  M1 = 'A_A',
  M2 = 'B_B',
  M3 = 'C_C',
}

console.log(A18);
console.log(A19);

函数

可以声明具名函数和匿名函数。

function b(x) { // 名字为b的函数
  return x;
}
const b1 = function (x) {  // 没有名字的函数声明,直接将函数值赋给变量b1
  return x;
}

给函数添加类型:

function b2(x: number): number {
  return x;
}

完整的函数类型:

const b3: (x: number) => number = function (x: number): number {
  return x;
}

函数的类型包含参数类型和返回值类型两个部分。函数类型中参数的名字只是方便读的,它可以是其他名字:

const b4: (y: number) => number = function (x: number): number {
  return x;
}

我们平常工作中可能比较常用直接在参数中展开参数对象的函数写法function b ({ m1, m2 })...,这种写法可以像下面这样定义类型:

interface B {
  m1: number;
  m2: string;
}

function b5 ({ m1, m2 }: B): void {
  console.log(m1, m2);
}

给函数表达式形式的函数定义类型可以这样:

interface B {
  m1: number;
  m2: string;
}

interface B1 {
  (x: B): void;
}

const b6: B1 = function ({ m1, m2 }) {
  console.log(m1, m2);
}

那如果是像下面这种给参数默认值,又展开参数的写法呢,怎么定义类型?

function b7 ({ m1, m2 } = {}) {
  console.log(m1, m2);
}

只要像下面这样写就可以了:

interface B {
  m1?: number;
  m2?: string;
}

function b7 ({ m1, m2 }: B = {}): void {
  console.log(m1, m2);
}

m1m2设置为可选属性,因为当没有给b7传入参数或者传入b7的参数为undefined的时候,会给参数一个默认值{}{}是没有属性m1m2的。

推断类型

const b8 = function (x: number): number {
  return x;
}

const b9: (x: number) => number = function (x) {
  return x;
}

看上面两个等式,只要等式的某一边有类型,TypeScript就能指出函数的类型。这被称为“上下文类型化”,是一种类型推断。

可选参数和默认参数

可选参数

function b10 (x: number, y?: number, z?: number): number {
  return 1;
}

b10(1);
b10(1, 2);
b10(1, 2, 3);

在参数名后面加上?表示该参数是可选的。可选的参数名必需放在必传的参数后面。

默认参数

function b11 (x: number, y = 2): number {
  return 1;
}

没有传这个位置的参数或者传了undefined的时候,就会使用等号后面的值作为参数的值。在所有必传参数后面的默认参数像可选参数一样,可传可不传。

剩余参数

可以使用...将参数集中起来:

function b12 (x: number, ...rest: number[]): void {
  console.log(x);
  console.log(rest);
}

b12(1, 2, 3); // 打印: 1 和 [2, 3]

上例中rest是传入函数的x参数之外的参数的数组。

this

在普通函数中的this指向的是函数执行时候的上下文对象。这在返回一个函数或者传递一个函数作为参数的时候会给人造成困惑。

情况1:返回一个函数

const b14 = {
  m1: 1,
  m2: function (): () => void { // 返回一个函数
    return function (): void {
      console.log(this.m1);
    };
  },
}

b14.m2()(); // 打印:undefined

这里的thiswindow对象(如果是严格模式的话,thisundefined),因为返回一个函数再执行,相当于在顶层作用域下执行:

function (): void {
    console.log(this.m1);
  };

在顶层作用域下执行方法的时候,默认会将它作为window对象的方法执行。所以这里的thiswindowwindow对象没有m1属性,所以会打印出undefined

情况2:传递一个函数作为参数

const b15 = {
  m1: 1,
  m2: function (x: () => void): void { // 将函数作为一个参数
    x();
  },
};

const b16 = function (): void {
  console.log(this.m1);
}

b15.m2(b16); // 打印:undefined

这里无论如何函数都是这样调用的x(),没有作为任何对象的方法调用(obj.method()),所以无论如何x()中的this都是window

对于情况1(返回一个函数),可以使用箭头函数解决这个问题,因为箭头函数捕获函数创建时候的this而不是函数调用时的this

const b14 = {
  m1: 1,
  m2: function (): () => void { // 返回一个函数
    return (): void => {
      console.log(this.m1);
    };
  },
}

b14.m2()(); // 打印:1

这里的this就是函数创建时候的this,即b14对象。

this 参数

打开noImplicitThis配置,在使用隐含any类型的this的时候会报错。打开noImplicitThis配置之后,下面这段代码,this的位置会报错:

const b14 = {
  m1: 1,
  m2: function (): () => void { // 返回一个函数
    return function (): void {
      console.log(this.m1); // 报错: 'this' implicitly has type 'any' because it does not have a type annotation.
    };
  },
}

b14.m2()(); // 打印:undefined

可以给this参数声明类型,this参数是函数参数列表的第一个位置的假参数。

interface B2 {
  m1: number;
  m2(this: B2): () => void;
}

const b17: B2 = {
  m1: 1,
  m2: function (this: B2): () => void {
    return (): void => {
      console.log(this.m1);
    }
  },
};

const b18 = b17.m2();
b18();
例-1

this的类型为B2

回调中的this参数

当你像例-1那样的函数作为回调函数传递给某个库的时候,这个库会作为普通函数来调用它,因为在TypeScript中,例-1的那种写法,this是假参数,实际上是不用真的传递一个this参数给b2的,但是在库中作为普通函数来调用,就会认为this参数是undefined

使用箭头函数解决这个问题,因为箭头函数的this是函数创建时候的上下文,所以直接像这样写就可以了:

class B3 {
  m1 = 1;
  m2 = (x: number) => {
    this.m1 = x;
  }
}

const b20 = new B3();
someLib.libMethod(b20.m2);

重载

在JavaScript中,根据传入参数的不同返回不同类型的值是很常见的现象。在类型系统中描述这种类型,需要提供一个重载列表,为同一个函数提供多个函数类型。

function b21(x: number): { m1: string };
function b21(x: string): number;
function b21 (x: any): any {
  if (typeof x === 'number') {
    return { m1: 'abc' };
  }
  if (typeof x === 'string') {
    return 123;
  }
}
console.log(b21(1)); // 打印:{ m1: "abc" }
console.log(b21('b')); // 打印:123

就像上例那样创建了一个重载列表,描述b21接收什么类型的参数,返回什么类型的值。function b21 (x: any): any不是重载列表的一部分。

TypeScript从上到下进行匹配,如果遇到了可以匹配的,就将该重载作为正确的重载,所以在排序的时候应该将最有可能的重载排在最前面。

TypeScript是JavaScript的超集,TypeScript支持一些JavaScript目前还没有的类相关的特性。

接下来的练习例子比较潦草,专注其中的语法,不用想这些类是用来做什么的。

class C {
  m1: string;
  constructor(x: string) {
    this.m1 = x;
  }
  m2(): void {
    console.log(this.m1);
  }
}

const c = new C('ccc');
c.m2(); // 打印:ccc

const c = new C('ccc');通过new C('ccc')构造了一个C类的实例。这调用了之前定义的构造函数,创建了一个有C形状的新对象,并且执行了构造函数来初始化它。

继承

类可以从基类继承属性和方法。

class C1 {
m1: string = 'memberM1OfC1';
m2 (): void {
  console.log('memberM2OfC1');
}
}

class C2 extends C1 {
m3: string = 'memberM3OfC2';
}

const c2 = new C2();
console.log(c2.m1);
c2.m2();

C2是一个派生自基类C1的派生类。派生类通常称为子类,基类通常称为父类。子类C2包含父类C1中的属性m1和方法m2

class C3 {
  m1: number;
  constructor (x) {
    this.m1 = x;
  }
  m2 (): void {
    console.log(this.m1);
  }
}

class C4 extends C3 {
  m3: number;
  constructor (x) {
    super(x);
    this.m3 = x;
  }
  m2 (): void {
    console.log(this.m1 + this.m3);
  }
}

const c3 = new C3(5);
c3.m2();  // 打印: 5

const c4 = new C4(5);
c4.m2(); // 打印:10

每一个包含构造函数的派生类必需调用super()来执行基类的构造函数。在构造函数中使用this来获取一个属性之前,必需调用super()函数。上例中派生类的方法(m2)覆盖了从基类继承来的方法。

public、private、protected

publicprivateprotected是类成员的三个修饰符。分别表示公有成员、私有成员、被保护的成员。

public

不用修饰符的时候默认是public,也可以显示地使用public

可以自由访问的成员是公有(public)成员。不论是这个类的实例,还是这个类的派生类的实例,都能访问这些成员。

class C5 {
  public m1: string; // 公有属性
  public constructor (x) { // 公有构造函数
    this.m1 = x;
  }
  public m2 (): void { // 公有方法
    console.log('c5');
  }
}

私有字段

class C6 {
  #m1: number = 1;
  constructor (x) {
    this.#m1 = x;
  }
  m2 (): void {
    console.log(this.#m1);
  }
}

const c6 = new C6(1);
c6.m2();
c6.#m1; // 报错:Property '#m1' is not accessible outside class 'C6' because it has a private identifier.

私有字段和通常的属性不同,它有一些规则:

(1)私有字段以#符号开头。

(2)私有字段的范围是它包含的类。即私有字段只能在包含它的类中被访问。

(3)不能对私有字段使用publicprivateprotected等可访问修饰符。

(4)即使是JavaScript用户,也无法在包含私有字段的类的外面访问甚至检测到私有字段。

ECNAScript private fields关于私有字段讲得比较详细。

private

TypeScript也能声明一个被标记为private的成员,它不能从包含它的类外部被获取到。

class C7 {
  private m1: number = 1;
  constructor (x) {
    this.m1 = x;
  }
  m2 (): void {
    console.log(this.m1);
  }
}

const c7 = new C7(1);
c7.m2();
c7.m1; // 报错:Property 'm1' is private and only accessible within class 'C7'.

对于私有成员和保护成员,当一个类型中有private/protected成员时,另一个类型必需包含源自同一个声明的private/protected成员,它们才被认为是兼容的。

class C8 {
  private m1: number;
  m2: string;
}

class C9 extends C8 {
  m3: string;
}

class C10 {
  private m1: number;
  m2: string;
}

let c8 = new C8();
const c9 = new C9();
const c10 = new C10();

c8 = c9;
c8 = c10; // 报错

需要强调的一点是这里说的是类型C9类继承了C8,所以它的类型中包含private成员m1,将它的实例赋给C8的实例不会有类型错误。C10类和C8类都包含私有成员m1和一个公有成员m2,但由于它的私有成员是自己的,不是来源于C8,所以将C10的实例赋值给C8的实例会报错:

Type 'C10' is not assignable to type 'C8'.
  Types have separate declarations of a private property 'm1'.

private修饰符修饰的私有成员】和【前面加上#表示的私有字段】都只能在包含它们的类中被访问。前者能修饰方法和属性,后者只能表示属性不能表示方法。

protected

protectedprivate类似,不同的是声明为protected的成员也能被派生类访问。注意是在派生类内被访问,protected成员不能被派生类的实例访问。

class C11 {
  protected m1: number = 123;
}

class C12 extends C11 {
  m2 (): void {
    console.log(this.m1);
  }
}

const c12 = new C12();
c12.m2(); // 打印:123
c12.m1; // 报错:Property 'm1' is protected and only accessible within class 'C11' and its subclasses.

一个构造函数也能被标记为protected,这意味着这个类不能在包含它的类外面被实例化,但是可以被扩展。

class C13 {
  m1: number;
  protected constructor (x) {
    this.m1 = x;
  }
}

class C14 extends C13 {
  constructor (x) {
    super(x);
  }
}

const c13 = new C13(1); // 报错:Constructor of class 'C13' is protected and only accessible within the class declaration.
const c14 = new C14(1);
console.log(c14.m1); // 打印:1

readonly

可以使用readonly关键字让属性只读,readonly属性必需在它们的声明或者构造函数中被初始化。

class C15 {
  readonly m1: number = 1;
  readonly m2: number;
  constructor (x) {
    this.m2 = x;
  }
  m3 (): void {
    this.m2 = 1; // 报错:Cannot assign to 'm2' because it is a read-only property.
  }
}

const c15 = new C15(2);
console.log(c15.m1); // 打印:1
console.log(c15.m2); // 打印:2

参数属性

上面那个例子中,C15中通过readonly m2: number;初始化了属性,又在构造函数中将传入的参数赋值给了m2。可以像下面这样直接在参数中创建和初始化成员:

class C16 {
  constructor (readonly m1: number) {
    console.log(this.m1);
  }
}
const c16 = new C16(123); // 打印:123

上面的代码等同于:

class C17 {
  readonly m1: number;
  constructor (x) {
    this.m1 = x;
  }
}

参数属性通过在构造函数参数前面加上可访问的修饰符(privateprotectedpublic)或者readonly或者两个的结合来声明。

比如:

class C18 {
  constructor (protected readonly m1: number) {
    console.log(this.m1);
  }
}

存取器

TypeScript提供了getter/setter作为拦截对象成员的访问的一种方式,让你能细粒度地控制一个成员在对象中如何被访问。

class C19 {
  private m1: number = 1;
  get m2(): number {
    return this.m1;
  }
  set m2(x) {
    this.m1 = this.m1 * x;
  }
}

const c19 = new C19();
c19.m2 = 3;
c19.m2 = 2;
console.log(c19.m2); // 打印:6

存取器需要注意以下两点:

  1. 存取器需要设置编译器的输出为ECMAScript5或更高版本。(猜测应该是和Object.defineProperty()有关,TypeScript的实现中可能用到了ES5中添加的新的对象方法Object.defineProperty())
  2. 有一个get但是没有set的存取器被判定为readonly

静态属性

类的静态(static)成员只能在类自身而不是类实例中可见。

class C20 {
  static m1: number = 1;
}
console.log(C20.m1);

抽象类

抽象类是可以从中派生出其他类的基类。抽象类不能被直接实例化。和接口(interface)不同,一个抽象类可以包含成员的执行细节。abstract关键字用于定义抽象类以及抽象类中的抽象方法。

abstract class C21 {
  abstract m1: number;
  abstract m2(x: number): number;
  m3 (): void {
    console.log('333');
  }
}

class C22 extends C21 {
  m1: number = 2;
  m2 (x: number): number {
    return x * this.m1;
  }
}

let c21: C21; // 创建抽象类型的引用是ok的
c21 = new C21(); // 不能创建抽象类型的实例,这里报错:Cannot create an instance of an abstract class.

const c22 = new C22();
console.log(c22.m1);
c22.m2(3);
c22.m3();

构造函数

当在TypeScript中创建一个类的时候,其实在同一时间创建了多个声明。

  1. 类实例的类型。
  2. 构造函数。在使用new创建实例的时候会调用这个函数。

另一种考虑类的方式是类有实例部分和静态部分。

class C23 {
  m1: number;
  constructor(x: number) {
    this.m1 = x;
  }
}

let c23: C23 = new C23(1);
const c23Maker: typeof C23 = C23;
c23 = new c23Maker(2);

typeof C23 拿到的是类本身的类型,这个类型包含C23的所有静态成员和构造函数。C23是类实例的类型。

把类作为一个接口来使用

一个类声明创建了代表类实例的类型和一个构造函数。因为类创建了类型,所以在一些和接口相同的地方可以作为接口来使用。

class C24 {
  m1: number;
  m2: number;
}

interface InterfaceC24 extends C24 {
  m3: number;
}

const c24: InterfaceC24 = {
  m1: 1,
  m2: 2,
  m3: 3
};

装饰器

装饰器为类声明和类成员提供了一种添加注释和元编程(meta-programming)语法的方式。

(以下这段摘自维基百科-编程

元编程(英语:Metaprogramming),又译超编程,是指某类计算机程序的编写,这类计算机程序编写或者操纵其它程序(或者自身)作为它们的资料,或者在运行时完成部分本应在编译时完成的工作。多数情况下,与手工编写全部代码相比,程序员可以获得更高的工作效率,或者给与程序更大的灵活度去处理新的情形而无需重新编译。

我对元编程的粗略理解是,能够编写我们的程序的程序。具体可以看看下文的装饰器例子。)

装饰器是TypeScript的实验性功能,在未来的版本中可能会发生改变。为了允许对装饰器的支持,必需在命令行或者tsconfig.json中允许experimentalDecorators编译器选项。

一个装饰器是一种特殊的声明方式,它可以被附加到一个类声明、方法、存取器、属性或是参数上。装饰器使用@expression的形式,expression是一个会在运行时带着装饰声明信息被调用的函数。

装饰器构成

多个装饰器能应用到一个声明中,使用一行或多行的形式:

@f @g x

或:

@f 
@g 
x

当多个装饰器应用到一个声明中的时候,和数学中的f(g(x))类似。

(还记得上文说到的元编程的粗略理解吗?'能够编写我们的程序的程序',这里我们使用了装饰器语法@f @g x,然后会按照类似f(g(x))这种方式来编写我们的程序。)

装饰器工厂

如果我们想要自定义一个装饰器如何应用到声明中,我们可以写一个装饰器工厂。一个装饰器工厂是一个简单地返回表达式的函数,返回的表达式会在运行时被装饰器调用。

function decoratorFactory(x: number) {
  return function (target) {
    // 装饰器内部,可以基于传入的x和装饰器的参数target进行一些处理
  };
}

装饰器评估

对于装饰器是如何应用到类内部的各种声明,有一个明确定义的顺序:

1.对于每一个实例成员,将应用参数装饰器,然后是方法、存取器或属性装饰器。

2.对于每一个静态成员,将应用参数装饰器、然后是方法、存取器或属性装饰器。

3.对于构造函数,将应用参数装饰器。

4.对于类,将应用类装饰器。

接下来让我们来看看类装饰器、方法装饰器、存取器装饰器、属性装饰器、参数装饰器分别是什么。

类装饰器

一个类装饰器在一个类声明前声明。类装饰器应用于类的构造函数、可以用于观察、修改或者代替一个类声明。类装饰器不能用在一个声明文件中或者其他任何环境上下文中(例如一个declare类中)。

类装饰器的表达式会在运行时作为一个函数被调用,它会用被装饰类的构造函数作为唯一的参数。

如果类装饰器返回一个值,它会用提供的构造函数替换类声明。(注意,如果你选择返回新的构造函数,你必须注意保持原始的原型(prototype),在运行时使用装饰器的逻辑不会为你做这些)。

@decoratorC25
class C25 {
  m1: number = 1;
}

function decoratorC25 (constructor: Function) {
  constructor.prototype.m2 = 2;
}

const c25 = new C25();
console.log(c25);

通过装饰器在构造函数的原型上添加了m2属性,打印出的结果是这样的:

方法装饰器

一个方法装饰器在一个方法声明前被声明。装饰器应用到方法的属性描述符上,并且可以用来观察、修改、或者替代一个方法定义。一个方法装饰器不能被用在声明文件中、重载中、或者在任何环境上下文(比如declare类)中。

方法装饰器的表达式在运行时作为一个函数被调用,包含下面三个参数:

  1. 对于静态成员,是类的构造函数;对于实例成员,是类的原型(prototype)。
  2. 成员的名称。
  3. 成员的属性描述符。(注意:属性描述符在你的脚本目标小于ES5的时候会是undefined

属性描述符的类型是这样的:

interface PropertyDescriptor {
    configurable?: boolean; // 可配置
    enumerable?: boolean; // 可枚举
    value?: any; // 值
    writable?: boolean; // 可写
    get?(): any; // 存取器-取值
    set?(v: any): void; // 存取器-存值
}

如果方法装饰器返回一个值,它会作为该方法的属性描述符被使用。(注意:返回值在脚本目标小于ES5的时候会被忽视)

class C26 {
  @writable(false)
  m1() {
    console.log('aaa');
  }
}

function writable (x) {
  return function (
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    return {
      writable: false,
    };
  }
}

const c26 = new C26();
c26.m1 = () => {
  console.log('bbb');
};

装饰器@writable(false)m1设置为不可写的,修改m1会在运行时报错:

存取器装饰器

一个存取器装饰器在一个存取器声明前被声明。存取器装饰器应用在存取器的属性描述符上,可以用来观察、修改、或者替换一个存取器的定义。一个存取器修饰符不能被用在声明文件中,或者在任何其他的环境上下文(例如一个declare类)中。

注意:TypeScript不允许同时装饰成员的getset存取器。该成员的所有装饰器必须应用到文档顺序的第一个存取器。这是因为装饰器应用于属性描述符,属性描述符将getset存取器组合在一起,而不是分开声明。

这个存取器装饰器会在运行时作为函数被调用,包含下面三个参数:

1.对于静态成员来说是类的构造函数;对于实例成员来说是类的原型(prototype)。

2.成员的名字。

3.成员的属性描述符。(注意,在脚本目标小于ES5的时候,属性描述符是undefined

如果存取器装饰器返回一个值,它会作为该成员的属性描述符使用。(注意,在脚本目标小于ES5的时候,返回值会被忽略)

class C27 {
  m1: number;
  constructor (x) {
    this.m1 = x;
  }

  @decoratorC27
  get m2() {
    return this.m1 + 5;
  }

  m3 () {
    console.log('666');
  }
}

function decoratorC27 (
  target: any,
  propertyKey: string,
  descriptor: PropertyDescriptor
) {
  target.m3();
}


const c27 = new C27(1); // 打印:666
console.log(c27.m2); // 打印 6

属性装饰器

属性装饰器在一个属性声明前声明。一个属性装饰器不能用在一个声明文件中,或者任何其他的环境上下文(比如一个declare类)中。

属性装饰器的表达式会作为一个函数在运行时被调用,使用以下两个参数:

1.对于静态成员来说是类的构造函数,对于实例成员来说是类的原型。

2.成员的名字。

(注意:因为TypeScript中初始化属性装饰器的机制,一个属性描述符不能被用作一个属性装饰器的参数。这是因为目前当定义原型的成员的时候,没有机制来描述一个实例属性,并且没有办法来观察或者修改属性的初始化。返回值也被忽视了。因此,一个属性装饰器只能用来观察类中已经明确声明了一个名字的属性)

class C28 {
  @decoratorC28
  m1: number = 1;

  m2 (): void {
    console.log('123');
  }
}
function decoratorC28 (
  target: any,
  propertyKey: string,
) {
  target.m2();
}

const c28 = new C28(); // 打印 123

参数装饰器

参数装饰器在参数声明前被声明。参数装饰器应用于类的构造函数声明或方法声明。参数装饰器不能用在声明文件中、重载中、或者在任何其他环境上下文(比如一个declare类)中。

参数装饰器的会作为函数在运行时被调用,带着以下三个参数:

1.对于静态成员来说是类的构造函数;对于实例成员来说是类的原型(prototype)。

2.成员的名字。

3.函数的参数列表中参数的序号索引。

(注意:参数装饰器只能被用来观察一个方法中被声明的参数。)

参数装饰器的返回值将被忽略。

class C29 {
  m1 (@decoratorC29 x: number) {
    console.log(x);
  }

  m2 () {
    console.log('456');
  }
}

function decoratorC29 (
  target: any,
  propertyKey: string | symbol,
  parameterIndex: number
) {
  target.m2();
}

目前babel还不支持参数装饰器,会报错:

TypeScript学习笔记
《TypeScript学习笔记-1-基础类型、字面量类型、类型声明》
《TypeScript学习笔记-2-联合类型&交叉类型、泛型、类型守卫、类型推断》
当前篇《TypeScript学习笔记-3-枚举、函数、类、装饰器》
下一篇《TypeScript学习笔记-4-模块、命名空间》
《TypeScript学习笔记-5-tsc指令、TS配置、部分更新功能、通用类型》