文章目录
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
类同时具备 fly
和 swim
方法,满足了 Flyable
和 Swimmable
接口的要求。
3. 接口约束的局限性
implements
并不会更改类的方法类型或行为。在以下代码中,s
参数没有被类型推断为 string
,需要显式声明类型:
interface Checkable {
check(name: string): boolean;
}
class NameChecker implements Checkable {
check(s) {
// 此处 s 没有类型,需显式声明
return s.toLowerCase() === "ok";
}
}
要避免意外类型问题,建议始终为接口方法提供明确的参数类型声明。
三、重写内建类型与原型链问题
1. 子类化内建类型的注意事项
在子类化 Error
、Array
等内建类型时,可能会遇到实例方法无法正常工作的情况。例如,继承自 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 的类继承与接口实现机制为开发者提供了强大的类型检查工具,但同时也需要理解其内在的工作机制。
推荐: