参考文章:

  1. TypeScript 中文手册——类(class)

一个简单的类

TypeScript 中类的写法与 C# 或 Java 等面向对象语言的差不多,一个简单的类示例如下:

  1. // 类
  2. class Person {
  3. // 属性
  4. name: string;
  5. // 构造函数
  6. constructor(name: string) {
  7. this.name = name;
  8. }
  9. // 方法
  10. sayHello(): void {
  11. console.log(`Hello, ${this.name}! `);
  12. }
  13. }
  14. // 实例对象
  15. const person = new Person('WJT20');
  16. person.sayHello(); // "Hello, WJT20! "

继承

继承的写法也和其他面向对象语言一样,以下是一个继承实例,我们创建一个 Postman 类继承 Person 类:

  1. class Person {
  2. ...
  3. }
  4. class Postman extends Person {
  5. constructor(name: string) {
  6. super(name); // 将name传给父类
  7. }
  8. // 重写sayHello()方法,但不影响父类
  9. sayHello(): void {
  10. console.log(`Hello, I am postman——${this.name}! `);
  11. }
  12. sendMail(): void {
  13. console.log(`${ this.name }: This is your mail`); // this.name来自父类
  14. }
  15. }
  16. const postman = new Postman('WJT20');
  17. postman.sayHello(); // 调用重写方法
  18. postman.sendMail(); // 调用自身方法

派生类(子类)包含了一个构造函数,它必须调用super(),它会执行基类(父类)的构造函数。 而且,在构造函数里访问 this 的属性之前,我们一定要调用super()。这个是 TypeScript 强制执行的一条重要规则。

修饰符

  1. 公有修饰符(public)

默认情况下,类中的所有成员都是使用public修饰符,也就是说 Person 类中属性和方法也可以这样写:

  1. class Person {
  2. public name: string;
  3. public constructor(name: string) {
  4. this.name = name;
  5. }
  6. public sayHello(): void {
  7. console.log(`Hello, ${this.name}! `);
  8. }
  9. }
  1. 私有修饰符(private)

使用private关键字修饰的成员,只能在该类的内部访问:

  1. class Person {
  2. public name: string;
  3. private info: string = 'none';
  4. public constructor(name: string, info?: string) {
  5. this.name = name;
  6. this.info = info || 'none';
  7. this.getMyInfo(); // 可调用
  8. }
  9. private getMyInfo(): void {
  10. console.log(`${this.name}'s info => ${this.info}`);
  11. }
  12. }
  13. const person = new Person('WJT20', 'A programmer. ');
  14. person.getMyInfo(); // 不可调用
  15. console.log(person.name); // 可访问
  16. console.log(person.info); // 不可访问
  1. 受保护修饰符(protected)

protected修饰符的行为与private修饰符类似,不同之处在于protected修饰的成员可以在子类中访问,而private则不能:

  1. class Person {
  2. public name: string;
  3. private privateInfo: string = 'none';
  4. protected protectedInfo: string = 'none';
  5. public constructor(name: string, info?: string) {
  6. this.name = name;
  7. this.privateInfo = info || 'none';
  8. this.protectedInfo = info || 'none';
  9. }
  10. }
  11. class Postman extends Person {
  12. constructor(name: string, info?: string) {
  13. super(name, info);
  14. console.log(this.privateInfo); // 不可访问
  15. console.log(this.protectedInfo); // 可访问
  16. }
  17. }
  18. const postman = new Postman('WJT20', 'A postman. ');
  1. 只读修饰符(readonly)

readonly修饰符可以将属性设置为只读类型,只读属性只能在声明或在构造函数内初始化:

  1. class Person {
  2. public name: string;
  3. readonly type: string;
  4. public constructor(name: string) {
  5. this.name = name;
  6. this.type = 'human'; // 赋值成功
  7. }
  8. }
  9. const person = new Person('WJT20');
  10. person.type = 'alien'; // 赋值失败
  1. 参数属性

通过给构造函数的参数添加修饰符,可以实现参数属性,仔细观察前面我写的代码,初始化属性我是这样做的:

  1. class Person {
  2. private name: string;
  3. public constructor(name: string) {
  4. // 根据构造函数传入的参数初始化属性
  5. this.name = name;
  6. }
  7. }

使用参数属性改造后:

  1. class Person {
  2. public constructor(private name: string) {}
  3. }

看上去是不是简洁了很多。

存取器机制

对于public修饰的类成员来说,无论是在类的内部还是外部,我们都可以控制这些成员的取值,例如:

  1. class Person {
  2. public constructor(public name: string) {}
  3. }
  4. const person = new Person('WJT20');
  5. console.log(person.name); // "WJT20"
  6. person.name = 'WJT21';
  7. console.log(person.name); // "WJT21"

某些时候,我们会用private修饰原本公有的成员,以保证数据的安全,只提供指定的存取器 api(getter/setter)给外部:

  1. class Person {
  2. public constructor(private _name: string) {}
  3. // getter
  4. get name(): string {
  5. return this._name;
  6. }
  7. // setter
  8. // 注意,setter不能指定类型
  9. set name(name: string) {
  10. this._name = name;
  11. }
  12. }
  13. const person = new Person('WJT20');
  14. console.log(person.name); // "WJT20"
  15. person.name = 'WJT21';
  16. console.log(person.name); // "WJT21"

静态属性

类的静态成员,指的是那些存在于类本身上面而不是类的实例上的类成员(前面我们接触到的都是存在于类实例上的),要定义静态成员,可以使用static关键字:

  1. class Person {
  2. static type: string = 'human'; // 静态属性type
  3. public constructor(private _name: string) {}
  4. ...
  5. }
  6. console.log(Person.type); // "human"

抽象类

抽象类做为其它派生类的基类使用。它们一般不会直接被实例化。抽象类中的抽象方法不包含具体实现并且必须在派生类中实现。使用abstract关键字可以定义抽象类和在抽象类内部定义抽象方法。

  1. abstract class Person {
  2. static type: string = 'human';
  3. abstract sayHello(): void; // 在派生类中实现,注意静态成员的写法
  4. constructor(public name: string) {}
  5. }
  6. class Postman extends Person {
  7. constructor(name: string) {
  8. super(name);
  9. }
  10. sayHello(): void {
  11. console.log(`Hello, I am ${this.name}`);
  12. }
  13. }
  14. const person = new Person('WJT20'); // 实例化失败
  15. const postman = new Postman('WJT20'); // 实例化成功
  16. postman.sayHello();

泛型

泛型函数

形如:

  1. function identity<T>(arg: T): T {
  2. return arg;
  3. }

函数identity()叫作泛型函数,其中 T 被称为类型变量(名称自定义,以下均命名为 T),它是一种特殊的变量,只用于表示类型而不是值。T 帮助我们捕获用户传入的类型,并允许在上下文中通过引用 T 来复用传入的类型。

调用identity()有两种方式:

  1. // 直接声明T的类型
  2. const res1 = identity<string>("Api");
  3. // 极简模式,由编译器自行判断T的类型
  4. const res2 = identity("Api");

需要注意的,如果入参传的是一个数组,T 表示的就是元素类型,而不是入参类型,所以出入参的类型表示应该是”T[]”而不是”T”:

  1. /** 编译错误 */
  2. function loggingIdentity<T>(arg: T): T {
  3. console.log(arg.length); // Error: T doesn't have .length
  4. return arg;
  5. }
  6. /** 编译正确 */
  7. function loggingIdentity<T>(arg: T[]): T[] {
  8. console.log(arg.length); // Array has a .length, so no more error
  9. return arg;
  10. }

泛型接口

有时候我们需要定义泛型的接口,以上文的identity()为例,可以有以下两种写法,使用方法略有不同:

1)

  1. interface Identity1 {
  2. <T>(arg: T): T;
  3. }
  4. function identity<T>(arg: T): T {
  5. return arg;
  6. }
  7. const myIdentity: Identity1 = identity;

2)

  1. interface Identity2<T> {
  2. (arg: T): T;
  3. }
  4. function identity<T>(arg: T): T {
  5. return arg;
  6. }
  7. const myIdentity: Identity2<number> = identity;

泛型类

泛型类的写法与泛型接口的写法相似,定义一个泛型类,可以这么写:

  1. class KeyString<T> {
  2. getNumber: (a: T, b: T) => T;
  3. }

KeyString 类一旦声明了 T 的类型,那么getNumber()函数的传参类型就必须与 T 一致,否则会编译报错:

  1. /** 编译错误 */
  2. const key = new KeyString<string>();
  3. key.getNumber = (a: number, b: number) => (a + b)
  4. console.log(key.getNumber(1, 2)); // error!
  5. /** 编译正确 */
  6. const key = new KeyString<number>();
  7. key.getNumber = (a: number, b: number) => (a + b)
  8. console.log(key.getNumber(1, 2)); // 3
  9. /** 编译正确 */
  10. const key = new KeyString<string>();
  11. key.getNumber = (a: string, b: string) => (a + b)
  12. console.log(key.getNumber('1', '2')); // "12"

泛型约束

泛型约束,顾名思义就是给泛型增加约束条件,那么如何增加约束条件呢?这里以上文的泛型函数loggingIdentity()为例,我们现在要给loggingIdentity()增加约束,出入参都必须要有 length 这个属性,可以这么做:

  1. interface Lengthwise {
  2. length: number;
  3. }
  4. function loggingIdentity<T extends Lengthwise>(arg: T): T {
  5. console.log(arg.length); // Now we know it has a .length property, so no more error
  6. return arg;
  7. }

下面是泛型约束中使用多个类型参数,更高级的用法示例:

  1. interface A<T> {
  2. p: T;
  3. }
  4. function say<T, U extends A<T>>(a: T, b: U) {
  5. console.log(a, b);
  6. }
  7. say(1, { p: 2 }); // 1 {p: 2}
  8. say('1', { p: 2 }); // error!
  9. say(1, { p: '2' }); // error!

约束<T, U extends A<T>>表示say()的第二个参数(A类型)的 p 属性和第一个参数类型一致,否则报错。