MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

TypeScript 继承与多态:extends 和 super 的深入解析

2021-01-102.2k 阅读

一、TypeScript 中的继承基础

在面向对象编程中,继承是一个核心概念。它允许我们基于现有的类创建新的类,新类(子类)可以继承现有类(父类)的属性和方法,并且还可以添加自己特有的属性和方法。在 TypeScript 里,通过 extends 关键字来实现继承。

1.1 简单的继承示例

// 定义一个父类 Animal
class Animal {
    name: string;
    constructor(name: string) {
        this.name = name;
    }
    speak() {
        console.log(`${this.name} makes a sound.`);
    }
}

// 定义一个子类 Dog,继承自 Animal
class Dog extends Animal {
    breed: string;
    constructor(name: string, breed: string) {
        // 调用父类的构造函数
        super(name);
        this.breed = breed;
    }
    bark() {
        console.log(`${this.name} barks.`);
    }
}

// 创建一个 Dog 实例
const myDog = new Dog('Buddy', 'Golden Retriever');
myDog.speak(); // 输出: Buddy makes a sound.
myDog.bark();  // 输出: Buddy barks.

在上述代码中,Dog 类继承了 Animal 类。Dog 类不仅拥有 Animal 类的 name 属性和 speak 方法,还新增了 breed 属性和 bark 方法。在 Dog 类的构造函数中,使用 super 关键字调用了父类 Animal 的构造函数,以初始化从父类继承的 name 属性。

1.2 继承中的属性和方法访问

子类可以访问父类的公有属性和方法。在 TypeScript 中,类的属性默认是公有的,可以在类的内部、子类以及类的实例外部访问。

class Parent {
    public publicProp: string = 'Public Property';
    protected protectedProp: string = 'Protected Property';
    private privateProp: string = 'Private Property';

    public publicMethod() {
        console.log('This is a public method.');
    }
    protected protectedMethod() {
        console.log('This is a protected method.');
    }
    private privateMethod() {
        console.log('This is a private method.');
    }
}

class Child extends Parent {
    accessProperties() {
        // 可以访问公有属性和方法
        console.log(this.publicProp);
        this.publicMethod();

        // 可以访问受保护的属性和方法
        console.log(this.protectedProp);
        this.protectedMethod();

        // 不能访问私有属性和方法
        // console.log(this.privateProp); // 报错
        // this.privateMethod(); // 报错
    }
}

const child = new Child();
child.accessProperties();
// 可以在外部访问公有属性和方法
console.log(child.publicProp);
child.publicMethod();

// 不能在外部访问受保护或私有属性和方法
// console.log(child.protectedProp); // 报错
// console.log(child.privateProp); // 报错
// child.protectedMethod(); // 报错
// child.privateMethod(); // 报错

在这个例子中,我们定义了 Parent 类,它有公有(public)、受保护(protected)和私有(private)的属性与方法。Child 类继承自 Parent 类,在 Child 类内部可以访问公有和受保护的属性与方法,但不能访问私有属性与方法。在类的外部,只能访问公有属性和方法。

二、extends 关键字的深入探讨

2.1 类继承中的类型兼容性

当一个类继承另一个类时,子类的类型与父类的类型是兼容的。这意味着在需要父类类型的地方,可以使用子类的实例。

class Shape {
    color: string;
    constructor(color: string) {
        this.color = color;
    }
}

class Circle extends Shape {
    radius: number;
    constructor(color: string, radius: number) {
        super(color);
        this.radius = radius;
    }
}

function printShape(shape: Shape) {
    console.log(`The shape has color ${shape.color}`);
}

const myCircle = new Circle('Red', 5);
printShape(myCircle); // 可以将 Circle 实例传递给需要 Shape 类型的函数

在上述代码中,Circle 类继承自 Shape 类。printShape 函数接受 Shape 类型的参数,而我们可以将 Circle 类的实例 myCircle 传递给这个函数,因为 Circle 类型与 Shape 类型兼容。

2.2 多重继承的替代方案

TypeScript 不支持传统的多重继承(一个类从多个父类继承),因为多重继承可能会导致菱形继承问题(也称为致命菱形问题),即一个类从多个父类继承相同的属性或方法,导致命名冲突和难以维护的代码。然而,TypeScript 提供了一些替代方案来实现类似多重继承的功能。

一种常用的方法是使用接口(interface)。接口可以被多个类实现,一个类可以实现多个接口,从而达到类似多重继承的效果。

interface Flyable {
    fly(): void;
}

interface Swimmable {
    swim(): void;
}

class Bird implements Flyable {
    fly() {
        console.log('The bird is flying.');
    }
}

class Duck implements Flyable, Swimmable {
    fly() {
        console.log('The duck is flying.');
    }
    swim() {
        console.log('The duck is swimming.');
    }
}

在这个例子中,Duck 类实现了 FlyableSwimmable 两个接口,这使得 Duck 类同时具备了飞行和游泳的能力,类似于从多个“父类”继承功能。

三、super 关键字的全面解析

3.1 在构造函数中使用 super

正如前面的例子所示,在子类的构造函数中,必须在使用 this 之前调用 super。这是因为 super 用于调用父类的构造函数,以初始化从父类继承的属性。

class Vehicle {
    wheels: number;
    constructor(wheels: number) {
        this.wheels = wheels;
    }
}

class Car extends Vehicle {
    brand: string;
    constructor(wheels: number, brand: string) {
        super(wheels);
        this.brand = brand;
    }
}

const myCar = new Car(4, 'Toyota');
console.log(`The ${myCar.brand} has ${myCar.wheels} wheels.`);

Car 类的构造函数中,先调用 super(wheels) 来初始化从 Vehicle 类继承的 wheels 属性,然后再初始化 Car 类特有的 brand 属性。

3.2 使用 super 调用父类方法

子类可以重写父类的方法,并且在重写的方法中使用 super 调用父类的原始方法。

class Animal {
    speak() {
        console.log('The animal makes a sound.');
    }
}

class Dog extends Animal {
    speak() {
        super.speak();
        console.log('The dog barks.');
    }
}

const myDog = new Dog();
myDog.speak();

在上述代码中,Dog 类重写了 Animal 类的 speak 方法。在 Dog 类的 speak 方法中,先调用 super.speak() 执行父类 Animalspeak 方法,然后再输出特定于 Dog 类的信息。

3.3 super 在静态方法中的使用

在 TypeScript 中,静态方法也可以被子类继承和重写。当在子类的静态方法中使用 super 时,它引用的是父类的静态方法。

class Parent {
    static staticMethod() {
        console.log('This is a static method in Parent.');
    }
}

class Child extends Parent {
    static staticMethod() {
        super.staticMethod();
        console.log('This is a static method in Child.');
    }
}

Child.staticMethod();

在这个例子中,Child 类重写了 Parent 类的静态方法 staticMethod。在 Child 类的静态方法中,通过 super.staticMethod() 调用了父类的静态方法,然后输出了特定于 Child 类的信息。

四、多态在 TypeScript 中的体现

4.1 基于继承的多态

多态是指同一个方法在不同的类中有不同的实现。在 TypeScript 中,通过继承和方法重写来实现多态。

class Shape {
    color: string;
    constructor(color: string) {
        this.color = color;
    }
    draw() {
        console.log('Drawing a shape.');
    }
}

class Circle extends Shape {
    radius: number;
    constructor(color: string, radius: number) {
        super(color);
        this.radius = radius;
    }
    draw() {
        console.log(`Drawing a circle with color ${this.color} and radius ${this.radius}.`);
    }
}

class Rectangle extends Shape {
    width: number;
    height: number;
    constructor(color: string, width: number, height: number) {
        super(color);
        this.width = width;
        this.height = height;
    }
    draw() {
        console.log(`Drawing a rectangle with color ${this.color}, width ${this.width}, and height ${this.height}.`);
    }
}

function drawShapes(shapes: Shape[]) {
    shapes.forEach(shape => {
        shape.draw();
    });
}

const circle = new Circle('Red', 5);
const rectangle = new Rectangle('Blue', 10, 5);
const shapesArray: Shape[] = [circle, rectangle];
drawShapes(shapesArray);

在上述代码中,Shape 类有一个 draw 方法,CircleRectangle 类继承自 Shape 类并各自重写了 draw 方法。drawShapes 函数接受一个 Shape 类型的数组,通过遍历数组并调用每个形状的 draw 方法,实现了多态。不同类型的形状(CircleRectangle)调用 draw 方法时会有不同的表现。

4.2 接口与多态

接口也可以用于实现多态。不同的类实现同一个接口,并且对接口中的方法有不同的实现。

interface Printable {
    print(): void;
}

class Book implements Printable {
    title: string;
    constructor(title: string) {
        this.title = title;
    }
    print() {
        console.log(`Printing book: ${this.title}`);
    }
}

class Magazine implements Printable {
    name: string;
    constructor(name: string) {
        this.name = name;
    }
    print() {
        console.log(`Printing magazine: ${this.name}`);
    }
}

function printItems(items: Printable[]) {
    items.forEach(item => {
        item.print();
    });
}

const book = new Book('TypeScript in Action');
const magazine = new Magazine('Tech Monthly');
const itemsArray: Printable[] = [book, magazine];
printItems(itemsArray);

在这个例子中,BookMagazine 类都实现了 Printable 接口,并重写了 print 方法。printItems 函数接受一个 Printable 类型的数组,通过调用每个对象的 print 方法,实现了多态。

五、继承与多态的最佳实践

5.1 合理使用继承层次

在设计类的继承层次时,要保持层次结构清晰和简洁。避免过深的继承层次,因为这会使代码难以理解和维护。一般来说,继承层次不应该超过三层。

例如,假设我们有一个 Vehicle 类,Car 类继承自 VehicleSportsCar 类继承自 CarElectricSportsCar 类继承自 SportsCar。这样的四层继承可能会使代码变得复杂,在适当的时候,可以考虑通过组合(将一个类作为另一个类的属性)来替代过深的继承。

5.2 遵循里氏替换原则

里氏替换原则是面向对象设计的一个重要原则,它指出:所有引用基类(父类)的地方必须能透明地使用其子类的对象。这意味着子类应该能够替换父类,并且程序的行为不会发生异常。

在实现继承和多态时,要确保子类的行为与父类的行为兼容。例如,如果父类的某个方法有特定的前置条件和后置条件,子类重写该方法时,应该保证满足这些条件,或者至少不会破坏程序的正确性。

5.3 利用抽象类和抽象方法

抽象类是不能被实例化的类,它通常包含一些抽象方法(只有声明,没有实现)。抽象类的主要作用是为子类提供一个通用的框架。

abstract class Animal {
    name: string;
    constructor(name: string) {
        this.name = name;
    }
    abstract speak(): void;
}

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

class Cat extends Animal {
    speak() {
        console.log(`${this.name} meows.`);
    }
}

在上述代码中,Animal 类是一个抽象类,它有一个抽象方法 speakDogCat 类继承自 Animal 类,并实现了 speak 方法。通过使用抽象类和抽象方法,可以强制子类提供特定的实现,从而保证代码的一致性和规范性。

六、常见问题与解决方案

6.1 子类构造函数忘记调用 super

在子类的构造函数中,如果忘记调用 super,TypeScript 会抛出错误。这是因为在使用 this 之前,必须先调用父类的构造函数来初始化继承的属性。

class Parent {
    value: number;
    constructor(value: number) {
        this.value = value;
    }
}

// 报错:在派生类构造函数中,必须先调用 super 方法
class Child extends Parent {
    constructor(value: number) {
        // 这里忘记调用 super(value)
        this.value = value;
    }
}

解决方案就是在子类构造函数中正确调用 super,传入合适的参数。

6.2 方法重写时类型不匹配

当子类重写父类的方法时,重写方法的参数类型和返回类型必须与父类方法兼容。

class Parent {
    calculate(a: number, b: number): number {
        return a + b;
    }
}

// 报错:此签名与“Parent.calculate”不兼容
class Child extends Parent {
    calculate(a: string, b: string): string {
        return a + b;
    }
}

在这个例子中,Child 类重写的 calculate 方法参数类型和返回类型与父类不兼容,导致报错。需要修改 Child 类的 calculate 方法,使其参数类型和返回类型与父类保持一致或者兼容。

6.3 多重继承相关问题

虽然 TypeScript 不支持传统的多重继承,但在使用接口等替代方案时,可能会遇到一些问题,比如命名冲突。

假设两个接口 AB 都定义了相同名称的方法 method

interface A {
    method(): void;
}

interface B {
    method(): void;
}

class C implements A, B {
    method() {
        // 这里会出现命名冲突问题,不知道该实现哪个接口的 method
        console.log('Implementing method...');
    }
}

解决方案可以是在实现类中对方法进行更明确的命名,或者通过其他方式区分不同接口中同名方法的功能。

七、与其他编程语言对比

7.1 与 Java 的对比

在继承方面,Java 和 TypeScript 有很多相似之处。两者都使用 extends 关键字来实现类的继承,并且子类都可以重写父类的方法。然而,Java 对继承的限制更为严格,比如一个类只能继承一个直接父类,但可以实现多个接口。TypeScript 同样不支持传统的多重继承,但在使用接口实现类似功能时,语法上相对更灵活一些。

在多态方面,Java 和 TypeScript 都通过方法重写和继承来实现多态。不过,Java 是一种静态类型语言,在编译时就会检查类型的兼容性,而 TypeScript 虽然也支持静态类型检查,但它是 JavaScript 的超集,在某些情况下可以像 JavaScript 一样动态运行,类型检查相对更灵活。

7.2 与 Python 的对比

Python 是一种动态类型语言,它通过 class 关键字定义类,使用 super() 来调用父类的方法。与 TypeScript 不同,Python 没有像 TypeScript 那样严格的类型声明。在继承方面,Python 支持多重继承,但多重继承可能会导致复杂的代码结构,这一点与 TypeScript 不支持传统多重继承有所不同。

在多态方面,Python 通过鸭子类型(如果它走起来像鸭子,叫起来像鸭子,那么它就是鸭子)来实现多态,而 TypeScript 主要通过继承和接口实现多态,更强调类型的明确性。

八、实际项目中的应用场景

8.1 代码复用与分层架构

在大型前端项目中,通常会采用分层架构,如表现层、业务逻辑层和数据访问层。继承可以用于在不同层次之间实现代码复用。例如,在数据访问层,可以定义一个基类来处理数据库连接和基本的数据操作,然后不同的数据访问类(如用户数据访问类、订单数据访问类)继承自这个基类,并根据具体需求重写或扩展方法。

8.2 组件化开发

在前端组件化开发中,多态非常有用。例如,我们可以定义一个基础的 UI 组件类,然后不同的具体组件(如按钮组件、输入框组件)继承自这个基础类,并根据自身特点重写渲染方法。这样,在使用这些组件时,可以通过统一的接口来操作不同类型的组件,实现多态。

8.3 插件系统

在一些插件系统中,继承和多态可以用于实现插件的扩展。定义一个插件基类,包含一些通用的方法和属性,然后不同的插件类继承自这个基类,并实现自己特定的功能。主程序可以通过统一的接口来加载和使用不同的插件,实现多态性。

通过对 TypeScript 中继承与多态的深入解析,我们了解了 extendssuper 的详细用法,以及它们在实际编程中的应用场景和最佳实践。合理运用继承与多态,可以使我们的代码更加模块化、可维护和可扩展,从而提高前端开发的效率和质量。