Skip to content

在我们掌握了 TypeScript 中类的基础之后,便能熟练地创建对象的蓝图。但随着应用复杂度的提升,我们常常会遇到一种新的设计挑战:我们想为一组相关的类提供一个共同的“基类”,这个基类应该包含一些共享的实现(代码),同时又强制所有子类必须提供它们各自独有的实现。

单纯的类做不到“强制”,因为它所有的东西都是具体的。而单纯的接口 (interface) 又做不到“共享实现”,因为它只定义契约,不包含任何代码。为了填补这一空白,TypeScript 提供了抽象类 (Abstract Classes)

抽象类是一种特殊的类,它充当着“半成品蓝图”的角色,为其他类提供了一个可供扩展的模板。它既能定义具体的实现,又能声明必须由子类完成的“抽象”部分。

什么是抽象类?

抽象类使用 abstract 关键字进行声明。它的核心特点在于可以包含两种类型的成员:

  1. 具体成员 (Concrete Members):与普通类一样,包含具体的实现。这些属性和方法会被所有子类继承。
  2. 抽象成员 (Abstract Members):同样使用 abstract 关键字声明,但不包含任何实现细节。它们只定义一个签名(方法名、参数、返回类型)。任何继承该抽象类的子类,都必须为这些抽象成员提供具体的实现。

让我们通过一个例子来理解。假设我们正在为一个公司构建 HR 系统,系统中存在不同类型的员工,如 DeveloperManager。他们都有一些共同点(如姓名和基本信息),但在计算年度奖金的方式上却截然不同。

这时,一个 Employee 抽象类便是完美的选择:

typescript
// 声明一个 Employee 抽象类
abstract class Employee {
  // 1. 具体属性,所有子类共享
  public name: string;

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

  // 2. 具体方法,所有子类共享
  public getProfile(): string {
    return `Employee: ${this.name}`;
  }

  // 3. 抽象方法,没有实现,必须由子类提供
  public abstract calculateAnnualBonus(salary: number): number;
}

这个 Employee 类清晰地定义了一个模板:

  • 每个员工都有 name 和一个 getProfile 方法。
  • 每个员工都必须有一种计算年度奖金 (calculateAnnualBonus) 的方式,但具体怎么算,基类并不关心,由各个子类自己决定。

抽象类的规则与使用

1. 不能被实例化

抽象类是一个不完整的“半成品”,因此你不能直接使用 new 关键字来创建它的实例。它的唯一用途就是被其他类继承。

typescript
// const employee = new Employee("John"); 
// 编译时错误: Cannot create an instance of an abstract class.

2. 必须实现所有抽象成员

任何继承自抽象类的子类,都必须为父类中所有的抽象成员提供具体的实现,否则 TypeScript 编译器会报错。

typescript
class Developer extends Employee {
  // 子类必须实现 calculateAnnualBonus 方法
  public calculateAnnualBonus(salary: number): number {
    // 开发者的奖金是薪水的 10%
    return salary * 0.1;
  }
}

class Manager extends Employee {
  // 子类必须实现 calculateAnnualBonus 方法
  public calculateAnnualBonus(salary: number): number {
    // 经理的奖金是薪水的 20%
    return salary * 0.2;
  }
}

const dev = new Developer("Alice");
const mgr = new Manager("Bob");

console.log(dev.getProfile()); // "Employee: Alice" (继承自基类)
console.log(dev.calculateAnnualBonus(100000)); // 10000 (子类自己的实现)

console.log(mgr.calculateAnnualBonus(200000)); // 40000 (子类自己的实现)

通过这种方式,抽象类为整个继承体系提供了一个统一的结构和行为保证,确保了没有任何一个“员工”类型会遗漏掉奖金计算的逻辑。

抽象类 vs. 接口:该如何抉择?

抽象类和接口在某些方面看起来很相似,都定义了其他类型必须遵守的契约。但它们的根本目的和能力有着本质区别。

特性抽象类 (abstract class)接口 (interface)
核心目的提供模板共享代码定义纯粹的契约形状
实现可以包含具体实现的方法和属性不能包含任何实现(只定义签名)
构造函数可以有构造函数不能有构造函数
继承关系子类使用 extends 关键字,并且只能继承一个抽象类类使用 implements 关键字,一个类可以实现多个接口
语义“是一个” (is-a) 的强关系,强调继承共享行为“能做” (can-do) 的能力关系,强调功能行为的约定

决策指南

  • 选择抽象类,当你...

    • 需要在一组紧密相关的类之间共享代码(方法实现、属性值)。
    • 想为这些类提供一个共同的、包含基础功能的基类。
    • 能确定这些类之间存在明确的“is-a”层次关系(例如,Dog is an Animal)。
  • 选择接口,当你...

    • 只需要定义一个行为契约,而不关心其具体实现。
    • 希望为不相关的类提供共同的功能(例如,ButtonHyperlink 都可以 implements Clickable)。
    • 希望一个类能够拥有多种“类型”或“能力”(例如,一个类可以同时实现 SerializableLoggable)。

总结

抽象类不仅仅是一种语法,更是一种强大的面向对象设计工具。它在“完全抽象的接口”和“完全具体的类”之间,提供了一个完美的平衡点。

它允许我们为一组相关的类定义一个共同的、可复用的骨架,同时通过强制子类实现抽象方法,来保证整个继承体系的完整性和一致性。当你需要为你的类层次结构提供一种“架构性指导”,确保所有派生类都遵循某个核心模板时,抽象类便是你最得力的盟友。

不知道说啥了很无语了