typescript(4)- class 详解 | 青训营笔记

187 阅读8分钟

这是我参与「第四届青训营 」笔记创作活动的第11天

「前言」

问题回顾

class Rectangle {
  width: number;
  height: number;

  constructor(width: number, height: number) {
    this.width = width;
    this.height = height;
  }

  getAreaFunction() {
    return function (this: Rectangle) { // error: 即使指定了 this 也依然报错
      return this.width * this.height;
    };
  }
}

在上一篇文章中留下来个问题,这里虽然指定了 this 类型,但在 function 定义函数的内部一般会当做 undefined (在 node 环境中),除非将函数赋值给其他的变量,改变 this 的指向

class Rectangle {
  width: number;
  height: number;

  constructor(width: number, height: number) {
    this.width = width;
    this.height = height;
  }

  getAreaFunction(this: Rectangle) {
    return (): number => {
      return this.width * this.height;
    };
  }
}

在这里 this 作为参数的声明应该在最外层的函数,而且内层的函数声明应该改为箭头函数,这样可以确保 this 指向正确地传递

「class 类型」

定义 class 会额外定义实例类型

在上面的例子中,隐式地蕴含了一条 规则每当定义一个类的时候,会额外的定义 类的实例类型 和一个构造函数(构造函数就是类)

这样也就意味着类的名字可以作为 类型注解 ,指定变量的类型

class Test { }

const t: Test = new Test();

而这个类型叫做 实例类型,顾名思义,当创建类的实例的时候可以使用这个类型约束
但我们将整个类作为变量赋值的时候则不能将 实例类型 作为类型注解,例如

class Test { }

const nTest: Test = Test; // 虽然能通过编译,但是没有实际意义
new nTest(); // error 

正确的赋值方式

class Test { }

const nTest: typeof Test = Test;
new nTest();

区别于 Test 实例类型,typeof TestTest 的类型,这个类型也是 new Function 创建出来的类型,所以也可以用 Function 赋值

const nTest: Function = Test;

类可以作为接口使用

class Father {
  money: number;
  getMoney() {
    this.money += 100;
  }
}

interface Son extends Father {
  score: number;
}

const s: Son = {
  money: 100,
  score: 60,
  getMoney() {
    this.money += 20;
  },
}

接口可以继承类的属性和方法类型,而接口不能重写父类的属性,可以重写父类的方法

class Father {
  money: number;
  getMoney() {
    this.money += 100;
  }
}

interface Son extends Father {
  money: number; // 这里保持和父类一样的类型声明,如果修改为其他类型会报错
  score: number;
  getMoney: () => number // 重写类的方法
}

const s: Son = {
  money: 100,
  score: 60,
  getMoney() {
    this.money += 20;
    return this.money;
  },
}

类实现接口

在 j s中,一个 class 只能继承自另一个 class,若其他类中的方法与属性也想继承,则很麻烦。而在 ts 中可以使用 implements 关键字来实现一些类共有方法属性的提取。

interface ITest1 {
  show: () => void;
}

interface ITest2 {
  show2: () => void;
}

class Test implements ITest1, ITest2 {
  show2: () => void;
  show() { }
}

在这个例子中 Test 实现了两个接口,如果在被实现的类中缺少对应的属性或者方法,会报错

「继承」

TypeScript强制执行的一条重要规则:

派生类包含了一个构造函数,它 必须 调用 super(),它会执行基类的构造函数。 而且,在构造函数里访问 this的属性之前,我们 一定要调用 super()

class Father { }

class Son extends Father {
  constructor() {
    super();
  }
}

我们可以使用父类的实例类型约束被子类创建出的实例

class Father { }

class Son extends Father { }

const s: Father = new Son();

虽然这里使用 Father,但它的值类型依然是 Son

就像声明函数的时候依然可以用实际返回值类型覆盖掉声明返回值类型

const fn: (num1: number, num2: number) => void = (num1: number, num2: number): number => num1 + num2;

但是不能覆盖掉参数的设置

「修饰符」

class Person {
  constructor(name) {
    this.name = name; // error,类型“Person”上不存在属性“name”
  }
}

我们在 class 类型使用 this的时候使用没有声明的属性会报错

所以我们定义类的成员属性的时候需要先定义成员属性类型

class Person {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
}

在类中定义的方法会被当做类原型上一个成员,所以在类中定义方法不需要声明,如果将方法定义在实例上(在 constructor 定义的方法),依然要声明类型

class Person {
  name: string;
  setName: (name: string) => void;
  constructor(name: string) {
    this.name = name;
    this.setName = function (name: string) {
      this.name = name;
    }
  }
}

ts 对类中每一个成员访问,设置了访问权限

通过 publicprivateprotected 这三个关键字可以设置每个成员的访问权限

  • public:任何地方都可以访问
  • private:只能在类内部访问
  • protected:和 private 相似, 但是还可以在子类中访问

public

任何地方都可以访问
对于 public 类型权限的成员,也可以省略掉 public
所以,没有添加修饰符的成员,会被当做 public

class Person {
  public name: string;
  public setName: (name: string) => void;
}

new Person().name; // undefined

private

不能在声明它的类的外部访问,包括子类

class Person {
  private name: string;
}
new Person().name; // error,属性“name”为私有属性,只能在类“Person”中访问

class P extends Person {
  constructor() {
    super();
    console.log(super.name); // error,属性“name”为私有属性,只能在类“Person”中访问。
  }
}

protected

可以在子类中访问,不能在类外访问

class Person {
  protected name: string;
}
new Person().name; // error,属性“name”受保护,只能在类“Person”及其子类中访问

class P extends Person {
  constructor() {
    super();
    console.log(super.name); // 可以访问
  }
}
注意
  1. 三种修饰符比较

TypeScript使用的是结构性类型系统。 当我们比较两种不同的类型时,并不在乎它们从何处而来,如果所有成员的类型都是兼容的,我们就认为它们的类型是兼容的。

然而,当我们比较带有 private或 protected成员的类型的时候,情况就不同了。 如果其中一个类型里包含一个 private成员,那么只有当另外一个类型中也存在这样一个 private成员, 并且它们都是来自同一处声明时,我们才认为这两个类型是兼容的。 对于 protected成员也使用这个规则。

class Person1 {
  private name: string;
}
class Person2 {
  private name: string;
}

let p1 = new Person1();
const p2 = new Person2();
p1 = p2; // error 不能将类型“Person2”分配给类型“Person1”。类型具有私有属性“name”的单独声明。

替换成 public 则可以进行赋值操作

  1. constructor 构造函数也可以添加修饰符
  • 添加 private 修饰符
class Person {
  private constructor() { }
}

class P extends Person {} // error,无法扩展类“Person”。类构造函数标记为私有。

new Person(); // error,类“Person”的构造函数是私有的,仅可在类声明中访问

constructor 构造函数添加了 private 修饰符,意味这个类在何处都不能使用,也不能被继承

  • 添加 protected 修饰符
class Person {
  protected constructor() { }
}

class P extends Person {} // 可以被继承

new Person(); // error,类“Person”的构造函数是受保护的,仅可在类声明中访问。

只能被继承,不能创建实例

readonly

使用 readonly关键字将属性设置为只读的。 只读属性必须在声明时或构造函数里被初始化。

class Octopus {
  readonly name: string;
  readonly numberOfLegs: number = 8;
  constructor(theName: string) {
    this.name = theName;
  }
}
let dad = new Octopus("Man with the 8 strong legs");
dad.name = "Man with the 3-piece suit"; // error,name 是只读的.

「static 静态成员」

我们可以创建类的静态成员,这些属性存在于 类本身 上面而不是类的实例上。

class Asian {
  static region: string = 'Asia';
  static say(): void {
    console.log('I come from Asia');
  }
}

Asian.say();

参数属性

在上个例子中,我们还用到 参数属性 的语法,我们可以在指定类成员类型的时候并且赋值

class Person {
  name: string = '张三';
}
console.log(new Person().name);

相当于

class Person {
  name: string;
  constructor() {
    this.name = '张三';
  }
}
console.log(new Person().name);

使用参数属性可以简化赋值操作

更加直观的写法

class Person {
  constructor(public name: string) { }
}

console.log(new Person('张三').name);

可以在构造函数的参数列表上额外加上传入参数的 修饰符,相当于进行了声明

「抽象类」

抽象类做为其它派生类的基类使用。 它们 不能直接被实例化。 不同于接口,抽象类可以包含成员的实现细节。 abstract关键字是用于定义抽象类和在抽象类内部定义抽象方法。

abstract class Animal {
  abstract name: string;
  abstract makeSound(): void;
  move(): void { }
}

class Horse extends Animal {
  name: string;
  makeSound(): void { }
}

抽象类中的抽象方法不包含具体实现并且 必须 在派生类中实现。

「存取器」

TypeScript支持通过getters/setters来截取对对象成员的访问。 它能帮助你有效的控制对对象成员的访问。

class Person {
  private _name: string;

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

  get name() {
    return this._name;
  }
}

new Person()._name; // error
const p = new Person();
p.name = '张三';
console.log(p.name); // 张三

通过 setget 关键字结合 private 关键字可以有效地设置访问权限,通过存储器设置的变量能提供外部修改和设置的权限

注意

只带有 get不带有 set的存取器自动被推断为 readonly

「tsc 转换 es6 之后的代码」

当我们使用 typescript 高级语法的时候,不能总是有效的将 ts 文件转化为 js 文件,而 tsc 命令只能将 ts 转为 es3 之前的代码,所以我们需要手动指定转化版本

tsc 文件名 -t es6

使用 -t esx 参数设置指定的版本 ,其中 x 为版本号

「参考文章」

类 · TypeScript中文网 · TypeScript——JavaScript的超集 (tslang.cn)