在我们掌握了 TypeScript 中类的基础之后,便能熟练地创建对象的蓝图。但随着应用复杂度的提升,我们常常会遇到一种新的设计挑战:我们想为一组相关的类提供一个共同的“基类”,这个基类应该包含一些共享的实现(代码),同时又强制所有子类必须提供它们各自独有的实现。
单纯的类做不到“强制”,因为它所有的东西都是具体的。而单纯的接口 (interface) 又做不到“共享实现”,因为它只定义契约,不包含任何代码。为了填补这一空白,TypeScript 提供了抽象类 (Abstract Classes)。
抽象类是一种特殊的类,它充当着“半成品蓝图”的角色,为其他类提供了一个可供扩展的模板。它既能定义具体的实现,又能声明必须由子类完成的“抽象”部分。
什么是抽象类?
抽象类使用 abstract 关键字进行声明。它的核心特点在于可以包含两种类型的成员:
- 具体成员 (Concrete Members):与普通类一样,包含具体的实现。这些属性和方法会被所有子类继承。
- 抽象成员 (Abstract Members):同样使用
abstract关键字声明,但不包含任何实现细节。它们只定义一个签名(方法名、参数、返回类型)。任何继承该抽象类的子类,都必须为这些抽象成员提供具体的实现。
让我们通过一个例子来理解。假设我们正在为一个公司构建 HR 系统,系统中存在不同类型的员工,如 Developer 和 Manager。他们都有一些共同点(如姓名和基本信息),但在计算年度奖金的方式上却截然不同。
这时,一个 Employee 抽象类便是完美的选择:
// 声明一个 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 关键字来创建它的实例。它的唯一用途就是被其他类继承。
// const employee = new Employee("John");
// 编译时错误: Cannot create an instance of an abstract class.2. 必须实现所有抽象成员
任何继承自抽象类的子类,都必须为父类中所有的抽象成员提供具体的实现,否则 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”层次关系(例如,
Dogis anAnimal)。
选择接口,当你...
- 只需要定义一个行为契约,而不关心其具体实现。
- 希望为不相关的类提供共同的功能(例如,
Button和Hyperlink都可以implements Clickable)。 - 希望一个类能够拥有多种“类型”或“能力”(例如,一个类可以同时实现
Serializable和Loggable)。
总结
抽象类不仅仅是一种语法,更是一种强大的面向对象设计工具。它在“完全抽象的接口”和“完全具体的类”之间,提供了一个完美的平衡点。
它允许我们为一组相关的类定义一个共同的、可复用的骨架,同时通过强制子类实现抽象方法,来保证整个继承体系的完整性和一致性。当你需要为你的类层次结构提供一种“架构性指导”,确保所有派生类都遵循某个核心模板时,抽象类便是你最得力的盟友。
