抽象类
本教程共 80 篇 · 第 34 篇 · 更新于 2026-08-10 · 约 11 分钟阅读
本节目标:掌握抽象类的定义和使用,理解”模板方法模式”的运作方式,能区分何时用抽象类、何时用接口。
上一章我们用继承来实现代码复用。但有时候,父类本身是不完整的——它定义了”骨架”,却把一些关键步骤留给子类去填充。这种”半成品”就是抽象类。
为什么需要抽象类
假设我们要写一个报表系统。每种报表都需要”获取数据 → 格式化 → 输出”这三个步骤,但不同报表的获取数据和格式化方式完全不同,只有”输出”这一步是共用的。
class PDFReport {
getData(): string[] {
return ["PDF数据1", "PDF数据2"];
}
format(data: string[]): string {
return data.join(" | ");
}
print(): void {
const data = this.getData();
const formatted = this.format(data);
console.log(`输出 PDF 报表:${formatted}`);
}
}
class ExcelReport {
getData(): string[] {
return ["Excel数据1", "Excel数据2"];
}
format(data: string[]): string {
return data.map((item) => `"${item}"`).join(",");
}
print(): void {
const data = this.getData();
const formatted = this.format(data);
console.log(`输出 Excel 报表:${formatted}`);
}
}
两个类里 print() 方法完全一样,复制粘贴了。正常人会想抽一个父类出来。但问题来了:父类的 getData() 和 format() 该写什么?
这就轮到抽象类登场了。
抽象类的基本语法
用 abstract 关键字声明抽象类和抽象方法:
abstract class Report {
abstract getData(): string[]; // 抽象方法:只声明,不实现
abstract format(data: string[]): string; // 抽象方法:只声明,不实现
print(): void { // 具体方法:有实现
const data = this.getData();
const formatted = this.format(data);
console.log(`输出报表:${formatted}`);
}
}
抽象方法只有签名,没有方法体。它跟接口里的方法声明很像,但区别在于它活在类里,可以跟具体方法共存。
子类继承抽象类后,必须实现所有抽象方法:
class PDFReport extends Report {
getData(): string[] {
return ["PDF数据1", "PDF数据2"];
}
format(data: string[]): string {
return data.join(" | ");
}
}
class ExcelReport extends Report {
getData(): string[] {
return ["Excel数据1", "Excel数据2"];
}
format(data: string[]): string {
return data.map((item) => `"${item}"`).join(",");
}
}
const pdf = new PDFReport();
pdf.print(); // 输出报表:PDF数据1 | PDF数据2
const excel = new ExcelReport();
excel.print(); // 输出报表:"Excel数据1","Excel数据2"
如果子类忘了实现某个抽象方法,TypeScript 会直接报错:
class BrokenReport extends Report {
// ❌ Non-abstract class 'BrokenReport' does not implement
// inherited abstract member 'getData' from class 'Report'.
}
抽象类不能直接实例化
const r = new Report(); // ❌ Cannot create an instance of an abstract class.
这很合理——抽象类本身就是不完整的,直接 new 出来调用抽象方法会炸。
模板方法模式
上面 Report 的例子就是经典的模板方法模式(Template Method Pattern)。抽象类定义算法的骨架(print()),子类负责填充具体步骤(getData()、format())。
这个模式在实际开发中非常常见。再举一个更贴近日常的例子——数据校验器:
abstract class Validator {
validate(value: string): string[] {
const errors: string[] = [];
if (!this.required(value)) {
errors.push("此项为必填");
}
const customErrors = this.doValidate(value);
errors.push(...customErrors);
return errors;
}
// 子类决定是否必填
protected required(value: string): boolean {
return true; // 默认必填
}
// 子类决定具体的校验逻辑
protected abstract doValidate(value: string): string[];
}
class EmailValidator extends Validator {
protected doValidate(value: string): string[] {
const errors: string[] = [];
if (!value.includes("@")) {
errors.push("邮箱格式不正确");
}
return errors;
}
}
class AgeValidator extends Validator {
protected doValidate(value: string): string[] {
const errors: string[] = [];
const age = Number(value);
if (isNaN(age) || age < 0 || age > 150) {
errors.push("请输入有效的年龄");
}
return errors;
}
}
const emailValidator = new EmailValidator();
console.log(emailValidator.validate("hello")); // ["邮箱格式不正确"]
const ageValidator = new AgeValidator();
console.log(ageValidator.validate("25")); // []
console.log(ageValidator.validate("200")); // ["请输入有效的年龄"]
父类 Validator 定义了校验流程:先检查必填,再执行子类自定义的校验。子类只需要实现 doValidate(),不用关心整个流程。
抽象类作为类型
抽象类不能实例化,但可以当类型用——接收任何子类的实例:
abstract class Shape {
abstract area(): number;
abstract perimeter(): number;
describe(): string {
return `面积:${this.area().toFixed(2)},周长:${this.perimeter().toFixed(2)}`;
}
}
class Circle extends Shape {
constructor(private radius: number) {
super();
}
area(): number {
return Math.PI * this.radius ** 2;
}
perimeter(): number {
return 2 * Math.PI * this.radius;
}
}
class Rectangle extends Shape {
constructor(private width: number, private height: number) {
super();
}
area(): number {
return this.width * this.height;
}
perimeter(): number {
return 2 * (this.width + this.height);
}
}
// 抽象类作为类型——可以接收任何子类
function printShapeInfo(shape: Shape): void {
console.log(shape.describe());
}
printShapeInfo(new Circle(5));
printShapeInfo(new Rectangle(4, 6));
抽象类 vs 接口
这是新手最常纠结的问题之一。两者确实有重叠,但核心区别很清晰:
| 维度 | 抽象类 | 接口 |
|---|---|---|
| 实例化 | ❌ 不能直接 new | ❌ 不能直接 new |
| 方法实现 | ✅ 可以有具体方法 | 只能声明(没有方法体) |
| 构造函数 | ✅ 可以有 | ❌ 不能有 |
| 访问修饰符 | ✅ public/protected/private | ❌ 只能 public |
| 属性 | ✅ 可以有初始化值 | ❌ 只能声明类型 |
| 继承/实现 | 单继承(extends 一个) | 多实现(implements 多个) |
| 编译后 | 保留为 JS 类 | 完全消失 |
选型指南
用抽象类:
- 你有共享的代码逻辑需要被子类复用(比如
print()方法)。 - 你需要 protected 成员,只让子类共享而外部不可见。
- 你需要构造函数来初始化公共状态。
用接口:
- 你只需要定义”这个类必须有什么方法/属性”,不需要共享代码。
- 你需要一个类同时满足多个契约(多实现)。
- 你想让完全不相关的类实现同一个契约。
Tip一个实用的判断方法:问问自己”这个基类有没有具体的方法需要被子类直接继承?“如果有,用抽象类;如果没有,用接口。
抽象构造签名
有时候你想写一个函数,接收”任意能产生 Shape 子类的构造函数”。直接写 typeof Shape 会报错——因为 Shape 是抽象的,不能 new:
abstract class Shape {
abstract area(): number;
}
// ❌ 不能直接 new 抽象类
function createShape(ctor: typeof Shape): Shape {
return new ctor(); // 报错
}
正确的写法是用构造签名 new () => Shape:
function createShape(ctor: new () => Shape): Shape {
return new ctor();
}
class Circle extends Shape {
area(): number { return 3.14; }
}
createShape(Circle); // ✅ 没问题
// createShape(Shape); // ❌ 抽象类不能传进来
new () => Shape 的意思是”一个能用 new 调用、返回 Shape 类型实例的构造函数”。抽象类的构造函数不满足这个签名,所以 TypeScript 会阻止你传入 Shape 本身。
小结
abstract class不能直接new,只能被继承。abstract method只有签名没有实现,子类必须实现。- 抽象类可以包含具体方法,子类直接继承使用。
- 模板方法模式是抽象类的经典应用:父类定义骨架,子类填充细节。
- 抽象类有共享代码时用,只用定义契约时用接口。
- 抽象类可以当类型使用,接收任意子类实例。
- 要接收抽象类的子类构造函数,用
new () => Type构造签名。
抽象类解决的是”继承层次中的公共代码复用”。但有些东西不属于任何实例,而是属于整个类——比如常量、工具函数、实例计数器。那是静态成员的地盘,下一章见。