Skip to content

Class 的主要结构只有构造函数属性方法访问符(Accessor,我们也只需要关注这几个部分即可。

基础类

typescript
class Animal {
  // 类的属性 实例成员
  name: string;
  age: number;

  // 构造函数
  constructor(name: string, age: number) {
    // 通过this访问类的成员。
    this.name = name;
    this.age = age;
  }

  // 类的方法
  makeSound(): void {
    console.log("Some generic sound");
  }
}

// 创建类的实例
const myAnimal = new Animal("Buddy", 3);

// 访问属性和调用方法
console.log(myAnimal.name); // Buddy
console.log(myAnimal.age);  // 3
myAnimal.makeSound();       // Some generic sound

属性的类型标注类似于变量,而构造函数、方法、存取器的类型编标注类似于函数:

typescript
class Foo {
  prop: string;

  constructor(inputProp: string) {
    this.prop = inputProp;
  }

  print(addon: string): void {
    console.log(`${this.prop} and ${addon}`)
  }

  get propA(): string {
    return `${this.prop}+A`;
  }

  // setter不允许标准返回值
  set propA(value: string) {
    this.prop = `${value}+A`
  }
}

唯一需要注意的是,setter 方法不允许进行返回值的类型标注,你可以理解为 setter 的返回值并不会被消费,它是一个只关注过程的函数。类的方法同样可以进行函数那样的重载。

就像函数可以通过函数声明函数表达式创建一样,类也可以通过类声明类表达式的方式创建。很明显上面的写法即是类声明,而使用类表达式的语法则是这样的:

typescript
const Foo = class {
  prop: string;

  constructor(inputProp: string) {
    this.prop = inputProp;
  }

  print(addon: string): void {
    console.log(`${this.prop} and ${addon}`)
  }
  
  // ...
}

类的修饰符

在 TypeScript 中,类的修饰符用于控制类的成员(属性和方法)的可见性和访问权限。

我们能够为 Class 成员添加这些修饰符:public / private / protected / readonly。除 readonly 以外,其他三位都属于访问性修饰符,而 readonly 属于操作性修饰符(就和 interface 中的 readonly 意义一致)。 这些修饰符应用的位置在成员命名前:

typescript
class Foo {
  private prop: string;

  constructor(inputProp: string) {
    this.prop = inputProp;
  }

  protected print(addon: string): void {
    console.log(`${this.prop} and ${addon}`)
  }

  public get propA(): string {
    return `${this.prop}+A`;
  }

  public set propA(value: string) {
    this.propA = `${value}+A`
  }
}

public

  • 如果没有指定修饰符,默认为 public
  • 公共成员可以在类内部和外部访问。
typescript
class MyClass {
  public myProperty: string;

  constructor(myProperty: string) {
    this.myProperty = myProperty;
  }
}

const instance = new MyClass("Hello");
console.log(instance.myProperty); // 可以访问

private

  • 当成员被标记成 private时,它就不能在声明它的类的外部访问。
typescript
class MyClass {
  private myPrivateProperty: string;

  constructor(myPrivateProperty: string) {
    this.myPrivateProperty = myPrivateProperty;
  }

  public getPrivateProperty(): string {
    return this.myPrivateProperty; // 在类的内部可以访问
  }
}

const instance = new MyClass("Secret");
// console.log(instance.myPrivateProperty); // 无法访问,会产生编译错误
console.log(instance.getPrivateProperty()); // 可以通过公共方法访问

protected

  • 使用 protected 修饰符限制成员在类的内部和继承类中访问,而在外部无法访问。
typescript
class MyBaseClass {
  protected myProtectedProperty: string;

  constructor(myProtectedProperty: string) {
    this.myProtectedProperty = myProtectedProperty;
  }
}

class MyDerivedClass extends MyBaseClass {
  public getProtectedProperty(): string {
    return this.myProtectedProperty; // 在继承类中可以访问
  }
}

const instance = new MyDerivedClass("Protected");
// console.log(instance.myProtectedProperty); // 无法访问,会产生编译错误
console.log(instance.getProtectedProperty()); // 可以通过继承类的公共方法访问

readonly

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

小结

  • public:此类成员在类、类的实例、子类中都能被访问。
  • private:此类成员仅能在类的内部被访问。
  • protected:此类成员仅能在类与子类中被访问,你可以将类和类的实例当成两种概念,即一旦实例化完毕(出厂零件),那就和类(工厂)没关系了,即不允许再访问受保护的成员

当不显式使用访问性修饰符,成员的访问性默认会被标记为 public。实际上,在上面的例子中,我们通过构造函数为类成员赋值的方式还是略显麻烦,需要声明类属性以及在构造函数中进行赋值。简单起见,我们可以在构造函数中对参数应用访问性修饰符

typescript
class Foo {
  constructor(public arg1: string, private arg2: boolean) { }
}

new Foo("linbudu", true)

此时,参数会被直接作为类的成员(即实例的属性),免去后续的手动赋值。

类的静态成员

在 TypeScript 中,类的静态成员属于类本身而不是类的实例, 用 static 关键字来声明类的静态成员。

typescript
class Foo {
  static staticHandler() { }

  public instanceHandler() { }
}

不同于实例成员,在类的内部静态成员无法通过 this 来访问,需要通过 Foo.staticHandler 这种形式进行访问。我们可以查看编译到 ES5 及以下 target 的 JavaScript 代码(ES6 以上就原生支持静态成员了),来进一步了解它们的区别:

typescript
var Foo = /** @class */ (function () {
    function Foo() {
    }
    Foo.staticHandler = function () { };
    Foo.prototype.instanceHandler = function () { };
    return Foo;
}());

静态成员直接被挂载在函数体上,而实例成员挂载在原型上,这就是二者的最重要差异:静态成员不会被实例继承,它始终只属于当前定义的这个类(以及其子类)。而原型对象上的实例成员则会沿着原型链进行传递,也就是能够被继承。


  • 静态成员在整个类的生命周期中只存在一份,它们不是通过类的实例来访问的,而是通过类名直接访问。
typescript
class MathOperations {
  static PI: number = 3.14;

  static calculateArea(radius: number): number {
    return this.PI * radius * radius;
  }
}

// 访问静态属性
console.log(MathOperations.PI); // 3.14

// 调用静态方法
const area = MathOperations.calculateArea(5);
console.log(area); // 78.5

PI 是一个静态属性,而 calculateArea 是一个静态方法。注意在静态方法内部,使用 this 关键字来引用类本身。

类的继承


类也支持继承,允许你创建一个类从另一个类派生。

与 JavaScript 一样,TypeScript 中也使用 extends 关键字来实现继承:

typescript
class Base { }

class Derived extends Base { }

对于这里的两个类,比较严谨的称呼是 基类(Base派生类(Derived。当然,如果你觉得叫父类与子类更容易理解也没问题。关于基类与派生类,我们需要了解的主要是派生类对基类成员的访问与覆盖操作


基类中的哪些成员能够被派生类访问,完全是由其访问性修饰符决定的。派生类中可以访问到使用 public 或 protected 修饰符的基类成员。除了访问以外,基类中的方法也可以在派生类中被覆盖,但我们仍然可以通过 super 访问到基类中的方法:

typescript
class Base {
  print() { }
}

class Derived extends Base {
  print() {
    super.print()
    // ...
  }
}

在派生类中覆盖基类方法时,我们并不能确保派生类的这一方法能覆盖基类方法,万一基类中不存在这个方法呢?所以,TypeScript 4.3 新增了 override 关键字,来确保派生类尝试覆盖的方法一定在基类中存在定义:

typescript
class Base {
  printWithLove() { }
}

class Derived extends Base {
  // 尝试覆盖
  override print() {
    // ...
  }
}

在这里 TS 将会给出错误,因为尝试覆盖的方法并未在基类中声明。通过这一关键字我们就能确保首先这个方法在基类中存在,同时标识这个方法在派生类中被覆盖了。


typescript
class Dog extends Animal {
  // 可以添加新的属性或方法
  breed: string;

  // 可以覆盖父类的方法
  makeSound(): void {
    console.log("Woof! Woof!");
  }

  // 子类构造函数需要调用父类构造函数
  constructor(name: string, age: number, breed: string) {
    // 调用父类的构造函数
    super(name, age);
    this.breed = breed;
  }
}

// 创建子类Dog的实例
const myDog = new Dog("Buddy", 3, "Golden Retriever");

myDog.makeSound(); // Woof! Woof!

类从基类中继承了属性和方法。 这里, Dog是一个 派生类,它派生自 Animal 基类,通过 extends关键字。 派生类通常被称作 子类,基类通常被称作 超类

抽象类

抽象类是对类结构与方法的抽象 简单来说,一个抽象类描述了一个类中应当有哪些成员(属性、方法等)一个抽象方法描述了这一方法在实际实现中的结构。我们知道类的方法和函数非常相似,包括结构,因此抽象方法其实描述的就是这个方法的入参类型返回值类型

抽象类使用 abstract 关键字声明:

typescript
abstract class AbsFoo {
  abstract absProp: string;
  abstract get absGetter(): string;
  abstract absMethod(name: string): string
}

抽象类中的成员也需要使用 abstract 关键字才能被视为抽象类成员,如这里的抽象方法。我们可以实现(implements)一个抽象类:

typescript
class Foo implements AbsFoo {
  absProp: string = "linbudu"

  get absGetter() {
    return "linbudu"
  }

  absMethod(name: string) {
    return name
  }
}

此时,我们必须完全实现这个抽象类的每一个抽象成员。需要注意的是,在 TypeScript 中无法声明静态的抽象成员


对于抽象类,它的本质就是描述类的结构。看到结构,你是否又想到了 interface?是的。interface 不仅可以声明函数结构,也可以声明类的结构:

typescript
interface FooStruct {
  absProp: string;
  get absGetter(): string;
  absMethod(input: string): string
}

class Foo implements FooStruct {
  absProp: string = "linbudu"

  get absGetter() {
    return "linbudu"
  }

  absMethod(name: string) {
    return name
  }
}

在这里,我们让类去实现了一个接口。这里接口的作用和抽象类一样,都是描述这个类的结构。除此以外,我们还可以使用 Newable Interface 来描述一个类的结构(类似于描述函数结构的 Callable Interface):

typescript
class Foo { }

interface FooStruct {
  new(): Foo
}

declare const NewableFoo: FooStruct;

const foo = new NewableFoo();

抽象类是 TypeScript 中的一种特殊类,它不能被实例化,只能被用作其他类的基类。抽象类经常用于定义一些通用的结构和行为,而由子类来具体实现这些结构和行为。

无法实例化

抽象类不能直接实例化,也就是说不能通过 new 关键字来创建抽象类的实例。它们存在于继承层次结构的顶部,作为其他类的基类。

包含抽象成员

抽象类可以包含抽象成员(属性或方法),这些成员在抽象类中只有声明而没有实际实现。子类必须提供这些抽象成员的具体实现。

typescript
abstract class Shape {
  abstract calculateArea(): number; // 抽象方法
}

作为基类

抽象类通常用于定义一些通用的结构和方法,但这些结构和方法的具体实现由子类来完成

typescript
class Circle extends Shape {
  radius: number;

  constructor(radius: number) {
    super();
    this.radius = radius;
  }

  calculateArea(): number {
    return Math.PI * this.radius * this.radius;
  }
}

提供共享的实现

抽象类可以包含一些具体实现的方法,这些方法在多个子类中可能是相同的。这样可以避免在每个子类中重复编写相同的代码。

typescript
abstract class Vehicle {
  // 共享方法
  startEngine(): void {
    console.log("Engine started");
  }
  // 抽象方法,不能有实现细节
  abstract drive(): void;
}

总的来说,抽象类在面向对象编程中起到了一种模板的作用,它定义了一些通用的结构和方法,为子类提供了一致的接口,并鼓励代码的重用和维护性。