第68课:类与访问修饰符——public/private/protected、抽象类、implements
TypeScript 对 JavaScript 的 class 进行了全面的类型增强。它不仅支持标准的类语法,还引入了访问修饰符(public、private、protected)来在编译时控制成员的可访问性,以及抽象类和接口实现(implements)来强制子类遵守特定的契约。本节课将逐一拆解这些特性的语法、语义和编译结果,并通过实际场景演示它们如何帮助构建更安全、更具约束力的面向对象代码。
1. 类的基本类型注解
TypeScript 中的类与 JavaScript 类的写法高度一致,但增加了对属性、构造函数参数和方法的类型注解。
1 | class User { |
关键约束:在 TypeScript 中,类的属性必须先声明(声明其名称和类型),然后才能在构造函数中通过 this.prop 赋值。这与 JavaScript 中可以动态添加属性不同——TypeScript 强制类的形状在编译时完全确定。如果开启 strictPropertyInitialization(包含在 strict: true 中),则每个属性还必须在构造函数中或通过初始化器得到赋值。
2. 访问修饰符:public、private、protected
访问修饰符决定了类成员在何处可以被访问。TypeScript 编译时执行这些检查,但不会在生成的 JavaScript 中添加任何运行时保护——修饰符仅存在于类型系统中。
2.1 public(默认)
所有未标注修饰符的成员默认为 public,可在任何地方访问。
1 | class Animal { |
2.2 private:仅类内部可访问
private 成员只能在该类的内部访问,实例和子类均不可访问。
1 | class BankAccount { |
TypeScript 的 private 只在编译时有效。编译为 JavaScript 后,balance 仍然是普通属性,运行时仍可通过 account['balance'] 访问。如果需要真正的运行时硬隐私,应使用 ES2022 的 # 私有字段。
1 | class SecureAccount { |
选择原则:
- 库/应用内部使用:
private(或protected)足够,编译时检查已能防止大部分误用。 - 需要绝对隐私(如安全数据):使用
#私有字段。 - 类型层面完全隔离:TypeScript 的
private还会影响类型兼容性——拥有private成员的两个不同类型不会被视为兼容,即使结构相同。
2.3 protected:类内部与子类可访问
protected 成员在 private 的基础上,还允许派生类(子类)访问。
1 | class Base { |
2.4 构造函数参数属性(Parameter Properties)
TypeScript 提供了一种简写语法:在构造函数参数前添加访问修饰符,可以同时声明属性、标注类型、并赋值。
1 | class User { |
这等价于:
1 | class User { |
构造函数参数属性是 TypeScript 中最常用的特性之一,极大减少了类的样板代码。
3. 抽象类(Abstract Classes)
使用 abstract 关键字声明的类不能直接实例化——它只能作为基类被继承。抽象类中可以包含抽象方法(无实现体,要求子类必须实现)和具体方法(有实现体,可被子类继承或重写)。
1 | abstract class Shape { |
抽象类的关键规则:
- 抽象方法必须在抽象类中声明。
- 子类必须实现所有抽象方法,否则自身也必须声明为抽象类。
- 抽象类可以有构造函数(通过
super()调用),可以有非抽象的成员。 - 抽象类可以声明
abstract属性访问器(getter/setter)。
4. implements:确保类满足接口契约
implements 关键字用于检查一个类是否实现了某个接口的所有成员。一个类可以实现多个接口(多重实现)。
1 | interface Flyable { |
implements 与 extends 的区别:
extends:继承实现(父类的方法和属性被子类获得)。implements:仅继承契约(类必须提供接口中声明的所有成员,但不继承任何实际代码)。
1 | interface Printable { |
使用场景:implements 常用于确保多个类共享相同的公共 API——例如不同的支付方式(WechatPay、Alipay)都实现 PaymentMethod 接口,从而可以在策略模式中互相替换。
5. 最佳实践与常见陷阱
5.1 优先使用 private,按需开放
默认将成员设为 private,仅在需要外部访问时才提升为 public 或 protected。这遵循“最小权限原则”,减少类之间的耦合。
5.2 private vs # 私有字段
| 特性 | private 修饰符 |
# 私有字段 (ES2022) |
|---|---|---|
| 生效时机 | 仅编译时检查 | 编译时 + 运行时硬隐私 |
| 能否被子类访问 | 否 | 否 |
运行时通过 obj[name] 访问 |
✅ 可以(失去类型保护) | ❌ 完全不可访问 |
| 类型兼容性影响 | 影响(private 成员不同 → 类型不兼容) |
影响(# 字段不同 → 类型不兼容) |
| 推荐场景 | 库的内部实现、应用层逻辑保护 | 高安全性数据、必须运行时隔离的场景 |
5.3 抽象类 vs 接口
| 对比维度 | 抽象类 | 接口 |
|---|---|---|
| 实例化 | 不能直接实例化 | 不能实例化 |
| 包含实现 | 可以包含具体方法和属性 | 仅声明,不能包含实现 |
| 继承 | 单继承(只能 extends 一个抽象类) |
多实现(可以 implements 多个接口) |
| 适用场景 | 共享部分实现,提供默认行为,约束子类必须实现某些方法 | 仅定义契约,不提供任何代码 |
课后练习
一、概念自测(选择题 / 填空题)
(单选) TypeScript 中,一个类实现多个接口应使用哪个关键字?
A.extends
B.implements
C.inherit
D.include(单选) 关于抽象类,以下描述正确的是?
A. 抽象类可以直接实例化。
B. 抽象类中只能包含抽象方法。
C. 子类继承抽象类后,必须实现所有抽象方法,否则自身也必须声明为抽象类。
D. 抽象类不能有构造函数。(填空) TypeScript 中,可以通过在构造函数参数前添加
______修饰符来同时声明并初始化类属性(语法糖)。(多选) 以下哪些是 TypeScript 支持的访问修饰符?
A.public
B.private
C.protected
D.internal
二、AI 编程任务:编写面向 AI 的提示词
场景:你需要使用 TypeScript 定义一个几何形状的类层次结构。要求如下:
- 定义一个抽象类
Shape,包含一个抽象方法getArea(): number和一个具体方法describe(): string(返回'该图形面积为 XXX'并调用getArea())。 - 定义
Circle类,继承Shape,通过构造函数接收radius(使用private修饰符),实现getArea。 - 定义
Rectangle类,继承Shape,通过构造函数接收width和height(使用private修饰符),实现getArea。 - 定义
Square类,继承Rectangle,其构造函数只接收一个side参数,调用super(side, side)。 - 定义一个接口
Drawable,包含draw(): void方法。 - 让
Circle实现Drawable,在draw中输出字符串描述。 - 添加 JSDoc 注释说明每个类和接口的用途。
任务要求:请写出一段完整的中文提示词,发送给 AI,使其生成符合上述要求的 TypeScript 代码。提示词中需明确指定抽象类的定义、继承关系、接口实现和访问修饰符的使用。
三、Agent 模式下的提示词示例
你是一个资深前端开发 Agent。请创建一个 TypeScript 文件
src/shapes.ts,定义几何形状的类层次结构。要求:
abstract class Shape:包含abstract getArea(): number和具体方法describe(): string,返回"该图形面积为 " + this.getArea().toFixed(2)。class Circle extends Shape:构造函数接收private radius: number,实现getArea()返回Math.PI * radius ** 2。class Rectangle extends Shape:构造函数接收private width: number, private height: number,实现getArea()返回width * height。class Square extends Rectangle:构造函数接收private side: number,调用super(side, side)。interface Drawable { draw(): void; },让Circle实现Drawable,draw()输出"绘制一个半径为 X 的圆形"。- 为每个类和方法添加 JSDoc 注释。在末尾添加示例代码:创建
Circle(5)、Rectangle(4, 6)、Square(3),分别调用describe()并打印,对Circle调用draw()。- 使用 ESM 导出所有类。完成后输出完整文件内容。
四、面试真题与参考答案
题目(字节跳动前端面试题):
请详细比较 TypeScript 中
interface和abstract class的异同。在什么场景下应优先选择抽象类,什么场景下优先选择接口?
参考答案:
| 对比维度 | interface |
abstract class |
|---|---|---|
| 实例化 | 不能实例化 | 不能实例化 |
| 包含实现 | 不能包含任何方法实现(仅声明签名) | 可以包含具体方法的实现和属性 |
| 访问修饰符 | 成员默认为 public,不支持 private/protected |
支持 public、private、protected |
| 继承/实现 | 一个类可以实现多个接口 | 一个类只能继承一个抽象类(单继承) |
| 编译产物 | 不生成任何 JavaScript 代码(仅用于类型检查) | 生成实际的 JavaScript 类代码 |
| 适用场景 | 定义纯粹的类型契约,不提供任何实现 | 多个子类需要共享部分实现,同时强制子类实现特定方法 |
选择原则:
- **优先使用
interface**:当仅需要描述对象的形状或定义类型契约时。interface更轻量、更灵活,且支持声明合并。 - 使用
abstract class:当一组相关的类需要共享代码(如工具方法、默认行为),同时又要强制子类提供特定方法的实现时。例如,框架中的BaseComponent提供通用的生命周期管理,但强制子类实现render方法。 - 两者结合:在大型项目中,通常用
interface定义公共 API 契约,用abstract class提供部分默认实现,实现类同时extends抽象类和implements接口。
课后练习答案
一、概念自测答案
B
- 解析:
implements用于类实现接口。extends用于类继承类或接口继承接口。
- 解析:
C
- 解析:抽象类不能实例化(A 错误),可以包含具体方法(B 错误),可以有构造函数(D 错误)。子类必须实现所有抽象方法,否则自身也必须为抽象类。
public(或private、protected、readonly)- 解析:构造函数参数属性语法(Parameter Properties)通过在参数前添加访问修饰符实现,例如
constructor(public name: string) {}。
- 解析:构造函数参数属性语法(Parameter Properties)通过在参数前添加访问修饰符实现,例如
A、B、C
- 解析:TypeScript 支持
public、private、protected三种访问修饰符。internal是 C# 的修饰符,TypeScript 中没有。
- 解析:TypeScript 支持
二、AI 编程任务参考答案(提示词示例)
示例提示词:
“请生成一个 TypeScript 文件,定义几何形状的类层次结构。要求:
- 抽象类
Shape,包含abstract getArea(): number和describe(): string方法(返回'该图形面积为 ' + this.getArea().toFixed(2))。Circle类继承Shape,构造函数参数private radius: number,实现getArea。Rectangle类继承Shape,构造函数参数private width: number, private height: number,实现getArea。Square类继承Rectangle,构造函数参数private side: number,调用super(side, side)。- 接口
Drawable包含draw(): void,Circle实现该接口。- 添加 JSDoc 注释和示例代码。直接输出完整代码。”