Dark Dwarf Blog background

TypeScript 类

TypeScript 类

TypeScript 完全支持 ES6 的 class,并在此基础上引入了更多强大的静态类型功能。

1. TypeScript 的增强功能

a.a. 访问修饰符

TypeScript 提供了三个访问修饰符来控制类成员(属性和方法)的可见性,这增强了类的封装性。

  • public (默认): 成员在任何地方都可以被访问。
  • protected: 成员只能在类自身及其子类中被访问。
  • private: 成员只能在类自身中被访问,即使是子类也无法访问。
class Person {
  public name: string; // 可在任何地方访问
  protected age: number; // 可在 Person 和其子类中访问
  private ssn: string; // 只能在 Person 中访问

  constructor(name: string, age: number, ssn: string) {
    this.name = name;
    this.age = age;
    this.ssn = ssn;
  }
}

class Employee extends Person {
  constructor(name: string, age: number, ssn: string) {
    super(name, age, ssn);
    console.log(this.name); // OK
    console.log(this.age); // OK
    // console.log(this.ssn); // Error: Property 'ssn' is private...
  }
}

b.b. private#private 的区别

需要特别区分 TypeScript 的 private 和 ECMAScript 标准的私有字段 (#):

  • private: 是一个编译时的检查。它在编译后的 JavaScript 代码中会被擦除,并不能阻止在运行时通过变通方法访问该属性。
  • # (私有字段): 是一个运行时的强制私有。编译后的代码会使用 WeakMap 等机制来确保该字段在外部绝对不可访问,提供了真正的封装。
class MyClass {
  private tsPrivate: string = "ts";
  #jsPrivate: string = "js";

  checkAccess() {
    console.log(this.tsPrivate); // OK
    console.log(this.#jsPrivate); // OK
  }
}

const instance = new MyClass();
console.log(instance.tsPrivate); // 编译时报错,但运行时可以访问 (instance['tsPrivate'])
// console.log(instance.#jsPrivate); // 编译时和运行时都会直接报错

最佳实践: 如果目标运行环境支持(ES2022+),优先使用 # 实现真正的运行时私有。

c.c. 只读修饰符 (readonly)

readonly 关键字确保属性只能在声明时或构造函数中被初始化,之后便不可修改。

class Octopus {
  readonly name: string;
  readonly numberOfLegs: number = 8;

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

  setName() {
    // this.name = "New Name"; // Error: Cannot assign to 'name' because it is a read-only property.
  }
}

d.d. 参数属性

为了简化类的初始化代码,TypeScript 允许在构造函数参数上直接使用修饰符(public, private, protected, readonly)。这会自动创建同名属性并完成赋值。

// 冗长的写法
class Point_Long {
  x: number;
  y: number;
  constructor(x: number, y: number) {
    this.x = x;
    this.y = y;
  }
}

// 使用参数属性的简洁写法
class Point_Short {
  constructor(
    public readonly x: number,
    public readonly y: number,
  ) {}
}

const p = new Point_Short(10, 20);
console.log(p.x); // 10

2. 抽象类

抽象类是作为其他类的基类而存在的,它们不能被直接实例化。抽象类可以包含抽象成员(方法或属性),这些成员没有具体实现,必须在派生类中被实现。

abstract class Shape {
  // 抽象属性
  abstract name: string;

  // 抽象方法
  abstract getArea(): number;

  // 普通方法
  printName() {
    console.log("Shape is: " + this.name);
  }
}

class Circle extends Shape {
  name = "Circle";
  radius: number;

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

  // 必须实现父类的抽象方法
  getArea() {
    return Math.PI * this.radius ** 2;
  }
}

// const s = new Shape(); // Error: Cannot create an instance of an abstract class.
const c = new Circle(5);
console.log(c.getArea());

3. 类与接口

a.a. 实现接口

implements 关键字用于声明一个类遵循某个接口的“契约”。它只检查类的实例部分是否满足接口的形状,但不提供任何实现

extends 用于继承实现,一个类只能继承一个父类;implements 用于确保形状,一个类可以实现多个接口。

interface Serializable {
  serialize(): string;
}

interface Loggable {
  log(message: string): void;
}

class User implements Serializable, Loggable {
  constructor(public name: string) {}

  serialize() {
    return JSON.stringify({ name: this.name });
  }

  log(message: string) {
    console.log(`[User: ${this.name}] ${message}`);
  }
}

b.b. 类作为类型使用

当一个类被声明时,TypeScript 会同时创建两个东西:

  1. 一个:即类的构造函数。
  2. 一个类型:代表该类实例的形状。
class Car {
  /* ... */
}

let carInstance: Car = new Car(); // `Car` 在这里被用作类型
let CarFactory: typeof Car = Car; // `typeof Car` 获取构造函数的类型

6. 高级主题

a.a. 类中的 this 问题

在 JavaScript 中,this 的指向在函数被调用时才确定。当类的方法作为回调函数传递时,this 的上下文会丢失,导致错误。

class MyComponent {
  message = "Hello";

  logMessage() {
    console.log(this.message); // `this` 期望是 MyComponent 实例
  }
}

const comp = new MyComponent();
const logger = comp.logMessage;

// logger(); // 运行时错误: Cannot read properties of undefined (reading 'message')

解决方案

  1. 箭头函数属性: 箭头函数会捕获其定义时的 this 上下文。

    class MyComponent {
      message = "Hello";
      logMessage = () => {
        console.log(this.message);
      };
    }
  2. 在构造函数中绑定 this: this.logMessage = this.logMessage.bind(this);

b.b. 泛型类

泛型允许我们创建可重用的类,这些类可以处理多种数据类型,同时保持类型安全。

class Box<T> {
  private content: T;

  constructor(initialContent: T) {
    this.content = initialContent;
  }

  getContent(): T {
    return this.content;
  }
}

const stringBox = new Box<string>("Hello, Generics!");
const numberBox = new Box(123); // 类型可被推断

console.log(stringBox.getContent().toUpperCase()); // OK, toUpperCase() 存在
console.log(numberBox.getContent().toFixed(2)); // OK, toFixed() 存在