本文献给:

已掌握 TypeScript 类基础、继承、多态、方法重写等知识的开发者。本文将带你学习抽象类(abstract)与抽象方法的定义和使用场景,以及抽象类与接口的区别,帮助你设计更合理的继承结构。


你将学到:

  1. 抽象类的定义与特点
  2. 抽象方法的语法与强制实现规则
  3. 抽象类不能实例化的原因
  4. 抽象类与接口的对比与选择
  5. 实际场景中的应用示例



一、抽象类(Abstract Class)

抽象类使用 abstract 关键字定义,用于作为其他类的基类。它不能直接被实例化,只能被子类继承。

abstract class Animal {
    name: string;
    constructor(name: string) {
        this.name = name;
    }
    move(): void {
        console.log(`${this.name} is moving`);
    }
}

// const a = new Animal("Creature"); // ❌ 不能实例化抽象类

class Dog extends Animal {
    bark() {
        console.log(`${this.name} barks`);
    }
}

const dog = new Dog("Buddy");
dog.move();  // "Buddy is moving"
dog.bark();  // "Buddy barks"

抽象类可以包含具体实现的方法(如上面的 move),也可以包含抽象方法。


二、抽象方法(Abstract Method)

抽象方法在抽象类中声明,但没有方法体。子类必须实现抽象方法(除非子类也是抽象类)。

abstract class Shape {
    abstract getArea(): number;   // 抽象方法,没有实现
    
    describe(): string {
        return `Area: ${this.getArea()}`;
    }
}

class Circle extends Shape {
    radius: number;
    constructor(radius: number) {
        super();
        this.radius = radius;
    }
    // 必须实现 getArea
    getArea(): number {
        return Math.PI * this.radius ** 2;
    }
}

class Square extends Shape {
    side: number;
    constructor(side: number) {
        super();
        this.side = side;
    }
    getArea(): number {
        return this.side ** 2;
    }
}

const shapes: Shape[] = [new Circle(5), new Square(4)];
shapes.forEach(s => console.log(s.describe()));

2.1 抽象方法的访问修饰符

抽象方法可以用 protectedpublic 修饰(默认 public)。子类实现时不能缩小访问权限。

abstract class Base {
    protected abstract init(): void;
}

class Derived extends Base {
    // 实现时可以是 public 或 protected,但不能是 private
    protected init() {
        console.log("initialized");
    }
}

2.2 抽象方法不能有实现

抽象方法不能包含方法体,甚至连空花括号都不行。

abstract class Demo {
    // abstract method() {}  // ❌ 不能有实现
    abstract method(): void; // ✅
}

三、抽象类的特点与约束

3.1 可以包含构造方法

抽象类可以有构造函数,用于初始化公共字段,供子类通过 super 调用。

abstract class Person {
    constructor(public name: string, public age: number) {}
    abstract introduce(): void;
}

class Student extends Person {
    constructor(name: string, age: number, public grade: number) {
        super(name, age);
    }
    introduce() {
        console.log(`I'm ${this.name}, ${this.age} years old, grade ${this.grade}`);
    }
}

3.2 可以包含具体方法

抽象类中既有抽象方法,也有具体实现的方法,这是抽象类与接口的最大区别之一。

3.3 子类必须实现所有抽象方法

如果子类没有实现所有抽象方法,该子类也必须声明为 abstract

abstract class Animal {
    abstract eat(): void;
    abstract sleep(): void;
}

class Dog extends Animal {
    eat() { console.log("Dog eats"); }
    // 缺少 sleep 方法 ❌
}

// 正确:子类也是抽象类
abstract class Mammal extends Animal {
    eat() { console.log("Mammal eats"); }
    // sleep 仍然抽象
}

四、抽象类 vs 接口

两者经常被拿来比较,因为都用于定义契约。但适用场景不同。

特性 抽象类 接口
实例化 不能 不能
方法实现 可以有具体方法 不能有实现(TS 接口只有声明)
字段(状态) 可以有实例字段 只能声明,不能初始化
构造函数 可以有 不能有
访问修饰符 支持 public/protected/private 所有成员隐含 public
多继承 一个类只能继承一个抽象类 一个类可以实现多个接口
适用场景 共享代码、状态、部分实现 描述能力/契约,轻量无状态

4.1 何时使用抽象类

  • 多个类有共同的状态(字段)和行为实现,但部分行为需要子类差异化实现。
  • 需要提供部分默认实现,减少子类重复代码。
  • 需要构造函数或 protected 成员。

4.2 何时使用接口

  • 只需要声明“能做什么”,不关心“怎么做”。
  • 一个类需要符合多个契约(多接口实现)。
  • 类型需要跨不同类层次结构(例如 ComparableSerializable)。
  • 为现有类补充类型定义(声明文件)。

4.3 示例:同时使用抽象类和接口

interface Flyable {
    fly(): void;
}

interface Swimmable {
    swim(): void;
}

abstract Animal {
    constructor(public name: string) {}
    abstract makeSound(): void;
}

class Duck extends Animal implements Flyable, Swimmable {
    makeSound() { console.log("Quack"); }
    fly() { console.log(`${this.name} flies`); }
    swim() { console.log(`${this.name} swims`); }
}

五、常见错误与注意事项

5.1 直接实例化抽象类

abstract class Base {}
// new Base();  // ❌ 无法创建抽象类的实例

5.2 子类未实现抽象方法

abstract class A {
    abstract test(): void;
}
class B extends A {}  // ❌ 非抽象类 B 未实现 test

5.3 抽象方法使用 private

abstract class C {
    private abstract method(): void;  // ❌ 抽象方法不能是 private
}

因为抽象方法需要在子类中实现,private 无法被子类访问。

5.4 抽象类中过度使用具体方法

抽象类如果大部分方法都是具体的,可以考虑是否应该设计为普通基类。抽象类的核心价值在于定义“不完整”的模板。

5.5 抽象类与构造函数中的抽象方法调用

在抽象类的构造函数中调用抽象方法是安全的,但要确保子类实现的方法不依赖于子类尚未初始化的字段。

abstract class Base {
    constructor() {
        this.init();  // 调用抽象方法
    }
    protected abstract init(): void;
}

class Derived extends Base {
    private data: string = "";
    protected init() {
        console.log(this.data);  // 此时 data 还是默认值 "",不是问题
    }
}

六、综合示例

// 抽象类:设备
abstract class Device {
    constructor(public brand: string, public model: string) {}
    
    // 具体方法
    turnOn(): void {
        console.log(`${this.brand} ${this.model} is turning on`);
    }
    
    turnOff(): void {
        console.log(`${this.brand} ${this.model} is turning off`);
    }
    
    // 抽象方法
    abstract performFunction(): void;
}

// 子类:手机
class Phone extends Device {
    constructor(brand: string, model: string, private os: string) {
        super(brand, model);
    }
    
    performFunction() {
        console.log(`Making a call from ${this.brand} ${this.model} (${this.os})`);
    }
    
    installApp(appName: string) {
        console.log(`Installing ${appName}`);
    }
}

// 子类:打印机
class Printer extends Device {
    constructor(brand: string, model: string, private pagesPerMinute: number) {
        super(brand, model);
    }
    
    performFunction() {
        console.log(`Printing document at ${this.pagesPerMinute} ppm`);
    }
    
    loadPaper(amount: number) {
        console.log(`Loaded ${amount} sheets`);
    }
}

// 使用多态
function operateDevice(device: Device) {
    device.turnOn();
    device.performFunction();
    device.turnOff();
}

const phone = new Phone("Apple", "iPhone 15", "iOS");
const printer = new Printer("HP", "LaserJet", 30);

operateDevice(phone);
operateDevice(printer);

// 检查类型
if (phone instanceof Phone) {
    phone.installApp("WhatsApp");
}
if (printer instanceof Printer) {
    printer.loadPaper(500);
}

// 抽象类作为数组元素类型
const devices: Device[] = [phone, printer];
devices.forEach(d => d.performFunction());

七、小结

概念 语法/示例 说明
抽象类 abstract class A {} 不能实例化,作为基类
抽象方法 abstract method(): void; 无方法体,子类必须实现
具体方法 抽象类中可以包含普通方法 提供默认实现
构造函数 抽象类可以有构造函数,供子类调用 用于初始化公共字段
vs 接口 抽象类有状态和部分实现,接口纯契约 按需选择
子类未实现抽象方法 子类必须也标记为 abstract 否则编译错误



觉得文章有帮助?别忘了:

👍 点赞 👍 – 给我一点鼓励
⭐ 收藏 ⭐ – 方便以后查看
🔔 关注 🔔 – 获取更新通知



标签: #TypeScript #抽象类 #抽象方法 #面向对象 #学习笔记 #前端开发

更多推荐