炼气九重天
第九节:接口与抽象
【教学目标】
- 理解接口(
interface)的核心作用:定义"数据结构契约"与"行为规范" - 掌握接口的定义语法,能通过接口约束对象、函数的结构
- 理解抽象类与抽象方法的概念,掌握
abstract关键字的使用规则 - 熟练区分接口与抽象类的差异,能在不同场景下正确选择使用
【本节重点】
- 接口的定义与使用:如何通过接口描述"属性类型"和"方法签名"
- 接口的核心特性:可选属性、只读属性、接口继承
- 抽象类与抽象方法:
abstract关键字的作用,抽象类的实例化限制 - 接口 vs 抽象类:两者的本质差异与典型应用场景
一、接口:定义"契约"的工具
接口(interface)本身不包含具体实现,仅用来定义"结构规范",就像一份"契约",要求遵循它的对象或类必须符合约定的结构(属性、方法的类型和名称)。
比如,定义一个"可移动"的接口 Movable,约定所有"可移动的事物"必须有 speed 属性和 move() 方法,至于具体是"汽车移动"还是"人移动",接口不关心,只要求符合规范。
1.1 接口的定义语法
用 interface 关键字定义接口,内部可包含属性声明 和方法签名(仅定义方法名、参数类型、返回值类型,无方法体)。
typescript
// 定义"用户"接口:约定用户对象的结构
interface User {
// 必选属性:必须有该属性,且类型为string
id: string;
name: string;
// 可选属性:用"?"表示,可存在可不存在
age?: number;
// 只读属性:用"readonly"表示,初始化后不能修改
readonly registerTime: string;
// 方法签名:仅定义方法结构,无实现
sayHi(): void;
// 带参数的方法签名
changeName(newName: string): string;
}
1.2 接口的使用场景
接口主要用于"类型约束",常见场景有两种:约束对象结构、约束函数结构,还可用于约束类的实现。
场景1:约束对象结构
当创建对象时,用接口指定对象的"模板",确保对象的属性、方法符合约定。
typescript
// 定义接口
interface User {
id: string;
name: string;
age?: number;
sayHi(): void;
}
// 创建对象时,遵循User接口约束
let user1: User = {
id: "001",
name: "张三",
// age是可选属性,可省略
sayHi(): void {
console.log(`大家好,我是${this.name}`);
}
};
// 错误示例:缺少必选属性id,不符合User接口约束
let user2: User = {
name: "李四", // 报错:Property 'id' is missing
sayHi(): void {}
};
场景2:约束函数结构
接口也可描述函数的"输入(参数)"和"输出(返回值)"类型,作为函数的"类型模板"。
typescript
// 定义"加法函数"接口:接收两个number参数,返回number
interface AddFunc {
(a: number, b: number): number;
}
// 定义函数时,遵循AddFunc接口约束
let add: AddFunc = (x, y) => {
return x + y;
};
console.log(add(1, 2).toString()); // 3
// 错误示例:参数类型不符合接口约束
let wrongAdd: AddFunc = (x: string, y: number) => { // 报错:x的类型应为number
return x + y;
};
场景3:约束类的实现(implements)
类通过 implements 关键字遵循接口,必须实现接口中定义的所有方法和属性(接口中的属性默认为 public)。
typescript
// 定义"可播放"接口
interface Playable {
play(): void;
pause(): void;
duration: number; // 必选属性
}
// 类实现接口:必须实现所有方法和属性
class Audio implements Playable {
duration: number; // 实现接口的属性
constructor(duration: number) {
this.duration = duration;
}
// 实现接口的方法
play(): void {
console.log("音频播放中");
}
pause(): void {
console.log("音频已暂停");
}
}
1.3 接口的核心特性
(1)可选属性与只读属性
- 可选属性 :用
?标记,对象中可包含也可省略该属性,适用于"非必需但可能存在"的场景(如用户的"年龄""地址")。 - 只读属性 :用
readonly标记,对象初始化后该属性不能被修改,适用于"创建后不可变"的数据(如用户的"注册时间""ID")。
typescript
interface Book {
readonly isbn: string; // 只读属性:初始化后不能改
title: string;
author: string;
publisher?: string; // 可选属性:可省略
}
let book1: Book = {
isbn: "9787111641247",
title: "JavaScript高级程序设计",
author: "尼古拉斯·泽卡斯"
};
book1.title = "JS高级程序设计"; // 正常:非只读属性可修改
book1.isbn = "123456"; // 报错:只读属性不能修改
(2)接口继承
接口可通过 extends 继承另一个或多个接口,实现"规范的复用与扩展",避免重复定义。
typescript
// 基础接口:定义"可移动"的规范
interface Movable {
speed: number;
move(): void;
}
// 继承Movable,扩展"可停止"的规范
interface Stoppable extends Movable {
stop(): void; // 新增方法签名
}
// 遵循Stoppable接口:需包含Movable的属性+方法,以及Stoppable的方法
let car: Stoppable = {
speed: 60,
move(): void {
console.log(`以${this.speed}km/h行驶`);
},
stop(): void {
console.log("停车");
}
};
(3)接口合并
同名接口会自动合并,所有成员会被合并为一个接口(注意避免成员冲突)。
typescript
// 定义第一个接口
interface Box {
width: number;
height: number;
}
// 定义同名接口(自动合并)
interface Box {
depth: number; // 新增属性
}
// 合并后:Box包含width、height、depth
let box: Box = {
width: 10,
height: 20,
depth: 15
};
二、抽象类:"半实现半规范"的模板
抽象类(用 abstract 修饰)是包含"抽象方法"的类,它既可以有"具体实现的属性和方法",也可以有"仅定义签名、无实现的抽象方法"。
抽象类的核心作用是作为"父类模板",要求子类必须实现抽象方法,同时提供可复用的具体逻辑。
2.1 抽象类与抽象方法的定义
- 用
abstract修饰类,该类即为抽象类。 - 用
abstract修饰类中的方法,该方法即为抽象方法(仅签名,无方法体)。
typescript
// 抽象类:动物(不能直接实例化)
abstract class Animal {
// 具体属性:所有动物共有的属性
name: string;
// 具体方法:所有动物共有的行为(有实现)
eat(): void {
console.log(`${this.name}在吃东西`);
}
// 抽象方法:仅定义签名,无实现,要求子类必须重写
abstract shout(): void;
// 构造函数:初始化具体属性
constructor(name: string) {
this.name = name;
}
}
2.2 抽象类的使用规则
- 抽象类不能直接实例化:抽象类是"模板",不是"具体类型",必须通过子类继承并实现抽象方法后,才能创建子类实例。
- 子类必须实现所有抽象方法:如果子类不实现父类的抽象方法,子类也必须定义为抽象类。
typescript
// 子类:Dog(非抽象类,必须实现shout方法)
class Dog extends Animal {
// 实现父类的抽象方法
shout(): void {
console.log(`${this.name}汪汪叫`);
}
}
// 子类:Cat(非抽象类,必须实现shout方法)
class Cat extends Animal {
// 实现父类的抽象方法
shout(): void {
console.log(`${this.name}喵喵叫`);
}
}
// 正确:创建子类实例
let dog = new Dog("小狗");
dog.eat(); // 输出:小狗在吃东西
dog.shout(); // 输出:小狗汪汪叫
// 错误:抽象类不能直接实例化
let animal = new Animal("动物"); // 报错:Cannot create an instance of an abstract class
2.3 抽象类与接口的结合使用
抽象类可实现接口,将接口的规范与抽象类的复用能力结合(抽象类实现接口时,可不必实现接口的方法,而由子类实现)。
typescript
// 定义"可游泳"接口
interface Swimmable {
swim(): void;
}
// 抽象类实现接口(无需实现接口方法)
abstract class AquaticAnimal extends Animal implements Swimmable {
// 抽象类可暂不实现接口方法,由子类实现
abstract swim(): void;
}
// 子类:Fish(实现所有抽象方法)
class Fish extends AquaticAnimal {
shout(): void {
console.log(`${this.name}吐泡泡`);
}
swim(): void {
console.log(`${this.name}在水里游`);
}
}
三、接口 vs 抽象类:核心差异与场景选择
接口和抽象类都能定义"规范",但本质和用途完全不同,需根据场景选择。
| 对比维度 | 接口(interface) |
抽象类(abstract class) |
|---|---|---|
| 本质 | 仅定义"契约",无任何实现 | 半实现半契约:有具体属性/方法,也有抽象方法 |
| 实例化 | 不能实例化,只能被"实现"或"约束" | 不能实例化,只能被"继承" |
| 继承/实现 | 一个类可实现多个接口(implements A, B) |
一个类只能继承一个抽象类(extends) |
| 访问修饰符 | 所有成员默认公开(无 private/protected) |
可使用 public/private/protected |
| 核心用途 | 约束对象/函数结构、定义跨类的通用规范 | 作为父类模板,复用具体逻辑+约束子类行为 |
场景选择建议
- 当需要跨类的通用规范 (如多个不相关的类需遵循同一套方法签名),用接口 。
例:Movable接口可被Car、Person、Bike等不相关的类实现。 - 当需要复用具体逻辑+约束子类 (如多个相关的类共享部分属性/方法),用抽象类 。
例:Animal抽象类的eat()方法可被Dog、Cat等相关子类复用,同时约束它们必须实现shout()。
【课堂小结】
- 接口是"契约",仅定义结构规范(属性类型、方法签名),无实现,可约束对象、函数、类的结构。
- 接口支持可选属性(
?)、只读属性(readonly),可通过extends继承其他接口,同名接口会自动合并。 - 抽象类是"半实现模板",包含具体属性/方法和抽象方法(
abstract),不能直接实例化,子类必须实现所有抽象方法。 - 一个类可实现多个接口,但只能继承一个抽象类;抽象类可实现接口并将接口方法的实现延迟到子类。
- 接口适用于跨类规范,抽象类适用于相关类的逻辑复用与约束。
【课后练习】
-
操作题:定义一个
Shape接口,包含width(宽度)、height(高度)属性,以及calculateArea()方法(返回面积,类型为number)。创建Rectangle(矩形)和Triangle(三角形)类,实现Shape接口,分别计算面积(矩形:宽×高,三角形:宽×高÷2)。 -
操作题:定义一个
Flyable抽象类,包含altitude(高度)属性、fly()抽象方法、land()具体方法(输出"降落")。创建Bird和Plane类继承Flyable,实现fly()方法(分别输出"鸟在飞翔"和"飞机在飞行")。 -
分析题:以下代码是否正确?为什么?
typescriptinterface Runable { speed: number; run(): void; } class Student implements Runable { speed: number = 5; run(): string { // 错误:返回值类型与接口不一致(接口要求void) return `以${this.speed}m/s跑步`; } } -
思考题:如果需要让一个类同时遵循"可移动"和"可发声"两个规范,应该用接口还是抽象类?如何实现?
【下节预告】
第十节将学习泛型(generic),这是ArkTS实现"代码复用与类型安全"的关键机制。我们将掌握:
- 泛型的定义语法:如何通过
<T>定义通用类型参数 - 泛型的核心用途:创建可复用的"类型无关"组件(如通用数组、通用函数)
- 泛型约束:如何通过接口限制泛型的类型范围
- 泛型的高级应用:泛型类、泛型接口的使用场景
通过泛型,我们能写出"一份代码适配多种类型"的灵活程序,同时保持类型检查的严格性,避免类型转换错误。