【TypeScript】深入理解 Class 继承与接口实现的关键特性

TypeScript 是一种在 JavaScript 基础上添加静态类型的编程语言,广泛应用于前端和后端开发。本文将详细介绍 TypeScript 中的类继承与接口实现(Class Heritage 和 Implements)等高级特性,帮助开发者更好地理解 TypeScript 的面向对象编程模式,提升代码的健壮性与可维护性。

一、Class 继承的基础概述

1. 什么是类继承?

在 TypeScript 和 JavaScript 中,类可以继承自另一个类,从而共享父类的属性和方法。这种继承模式可以让我们通过定义一个基础类,将公共逻辑抽象出来,然后在子类中进一步扩展或修改父类的方法。例如:

class Animal {
    
    
  move() {
    
    
    console.log("Moving along!");
  }
}

class Dog extends Animal {
    
    
  woof(times: number) {
    
    
    for (let i = 0; i < times; i++) {
    
    
      console.log("woof!");
    }
  }
}

const d = new Dog();
d.move(); // 父类方法
d.woof(3); // 子类方法

在此示例中,Dog 类继承了 Animal 类,具备了 move 方法,同时新增了 woof 方法。这种继承结构便于代码复用和模块化。

2. 使用 extends 关键字

在 TypeScript 中,extends 关键字用于实现类继承的关系。子类不仅可以继承父类的属性和方法,还可以进行方法重写。重写时可使用 super 关键字来调用父类的方法,确保遵循父类的行为。例如:

class Base {
    
    
  greet() {
    
    
    console.log("Hello, world!");
  }
}

class Derived extends Base {
    
    
  greet(name?: string) {
    
    
    if (name === undefined) {
    
    
      super.greet();
    } else {
    
    
      console.log(`Hello, ${
      
      name.toUpperCase()}`);
    }
  }
}

const d = new Derived();
d.greet(); // 调用父类的 greet 方法
d.greet("reader"); // 调用子类重写后的 greet 方法

在该示例中,Derived 类重写了 greet 方法,实现了更灵活的功能。

3. 初始化顺序与 super 关键字

JavaScript 类的初始化顺序可能会导致一些初始化顺序的问题,理解这一过程至关重要。在继承关系中,初始化顺序如下:

  • 父类字段初始化
  • 父类构造函数执行
  • 子类字段初始化
  • 子类构造函数执行

因此,以下代码会输出 "base" 而非 "derived"

class Base {
    
    
  name = "base";
  constructor() {
    
    
    console.log("My name is " + this.name);
  }
}

class Derived extends Base {
    
    
  name = "derived";
}

const d = new Derived(); // 输出 "My name is base"

由于父类构造函数先于子类字段初始化,因此 this.name 在父类构造函数中输出的仍然是 "base"

二、接口实现与 implements 关键字

1. 使用接口检查类型约束

接口(Interface)在 TypeScript 中扮演重要角色,它为类定义了一种契约(contract)。通过 implements 关键字,TypeScript 可以确保类符合接口的结构和行为。例如:

interface Pingable {
    
    
  ping(): void;
}

class Sonar implements Pingable {
    
    
  ping() {
    
    
    console.log("ping!");
  }
}

class Ball implements Pingable {
    
    
  pong() {
    
    
    console.log("pong!");
  }
  // 由于没有实现 ping 方法,编译时会报错
}

在这个例子中,Sonar 类成功实现了 Pingable 接口,但 Ball 类由于没有 ping 方法而无法通过编译。implements 仅作为类型检查,不会对类进行任何实际改动。

2. 同时实现多个接口

TypeScript 允许类同时实现多个接口,这在多继承受限的语言中尤其有用。例如:

interface Flyable {
    
    
  fly(): void;
}

interface Swimmable {
    
    
  swim(): void;
}

class Duck implements Flyable, Swimmable {
    
    
  fly() {
    
    
    console.log("Duck flying");
  }
  swim() {
    
    
    console.log("Duck swimming");
  }
}

此处的 Duck 类同时具备 flyswim 方法,满足了 FlyableSwimmable 接口的要求。

3. 接口约束的局限性

implements 并不会更改类的方法类型或行为。在以下代码中,s 参数没有被类型推断为 string,需要显式声明类型:

interface Checkable {
    
    
  check(name: string): boolean;
}

class NameChecker implements Checkable {
    
    
  check(s) {
    
     // 此处 s 没有类型,需显式声明
    return s.toLowerCase() === "ok";
  }
}

要避免意外类型问题,建议始终为接口方法提供明确的参数类型声明。

三、重写内建类型与原型链问题

1. 子类化内建类型的注意事项

在子类化 ErrorArray 等内建类型时,可能会遇到实例方法无法正常工作的情况。例如,继承自 Error 的自定义错误类可能无法正确调用实例方法:

class MsgError extends Error {
    
    
  constructor(m: string) {
    
    
    super(m);
  }
  sayHello() {
    
    
    return "hello " + this.message;
  }
}

const err = new MsgError("error message");
err.sayHello(); // 可能报错

这是因为内建类型的原型链行为在 TypeScript 和 JavaScript 中处理不同。可以通过手动设置原型来解决:

class MsgError extends Error {
    
    
  constructor(m: string) {
    
    
    super(m);
    Object.setPrototypeOf(this, MsgError.prototype);
  }

  sayHello() {
    
    
    return "hello " + this.message;
  }
}

此代码通过 Object.setPrototypeOf 显式设置了原型,使 MsgError 实例能正常访问原型方法 sayHello

2. 在不同环境中的兼容性

在不支持 Object.setPrototypeOf 的环境(如 IE10 及以下)中,上述方案无法正常工作。可通过将方法直接赋值到实例上以确保兼容性,但无法完全解决原型链的问题。

四、继承与接口实现的常见误区

1. implements 不会改变类行为

implements 关键字不会更改类的运行时行为,它只是在编译时检查类是否满足接口。例如以下代码:

interface A {
    
    
  x: number;
  y?: number;
}

class C implements A {
    
    
  x = 0;
}

const c = new C();
c.y = 10; // 编译报错,因为 y 是可选属性,但在 C 中并不存在

2. 避免方法签名不匹配

在继承过程中,子类必须遵循父类方法的签名,否则会导致错误。例如:

class Base {
    
    
  greet() {
    
    
    console.log("Hello, world!");
  }
}

class Derived extends Base {
    
    
  greet(name: string) {
    
    
    console.log(`Hello, ${
      
      name.toUpperCase()}`);
  }
}
// 编译报错,Derived 的 greet 与 Base 的签名不匹配

3. 使用 declare 避免重复声明

对于 ES2022 及以上的环境,当子类仅需声明而不改变父类字段的类型时,可以使用 declare 关键字避免重复声明。例如:

interface Animal {
    
    
  dateOfBirth: any;
}

interface Dog extends Animal {
    
    
  breed: any;
}

class AnimalHouse {
    
    
  resident: Animal;
  constructor(animal: Animal) {
    
    
    this.resident = animal;
  }
}

class DogHouse extends AnimalHouse {
    
    
  declare resident: Dog;
  constructor(dog: Dog) {
    
    
    super(dog);
  }
}

以上示例中,declare 仅用于类型检查,避免子类 DogHouse 重复定义 resident 字段。

总结

TypeScript 的类继承与接口实现机制为开发者提供了强大的类型检查工具,但同时也需要理解其内在的工作机制。

推荐:


在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/lph159/article/details/143501934