WinddSnow

TypeScript-Classes-and-Access-Modifiers

字数统计: 3.3k阅读时长: 13 min
2026/07/30

第68课:类与访问修饰符——public/private/protected、抽象类、implements

TypeScript 对 JavaScript 的 class 进行了全面的类型增强。它不仅支持标准的类语法,还引入了访问修饰符publicprivateprotected)来在编译时控制成员的可访问性,以及抽象类接口实现implements)来强制子类遵守特定的契约。本节课将逐一拆解这些特性的语法、语义和编译结果,并通过实际场景演示它们如何帮助构建更安全、更具约束力的面向对象代码。


1. 类的基本类型注解

TypeScript 中的类与 JavaScript 类的写法高度一致,但增加了对属性构造函数参数方法的类型注解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class User {
name: string;
age: number;

constructor(name: string, age: number) {
this.name = name;
this.age = age;
}

greet(): string {
return `Hello, I'm ${this.name}, ${this.age} years old.`;
}
}

const alice = new User('Alice', 25);
console.log(alice.greet()); // 'Hello, I'm Alice, 25 years old.'

关键约束:在 TypeScript 中,类的属性必须先声明(声明其名称和类型),然后才能在构造函数中通过 this.prop 赋值。这与 JavaScript 中可以动态添加属性不同——TypeScript 强制类的形状在编译时完全确定。如果开启 strictPropertyInitialization(包含在 strict: true 中),则每个属性还必须在构造函数中或通过初始化器得到赋值。


2. 访问修饰符:publicprivateprotected

访问修饰符决定了类成员在何处可以被访问。TypeScript 编译时执行这些检查,但不会在生成的 JavaScript 中添加任何运行时保护——修饰符仅存在于类型系统中。

2.1 public(默认)

所有未标注修饰符的成员默认为 public,可在任何地方访问。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Animal {
public name: string;

public constructor(name: string) {
this.name = name;
}

public makeSound(): void {
console.log(`${this.name} makes a sound.`);
}
}

const cat = new Animal('Kitty');
console.log(cat.name); // ✅ 外部可访问

2.2 private:仅类内部可访问

private 成员只能在该类的内部访问,实例和子类均不可访问。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class BankAccount {
private balance: number;

constructor(initialBalance: number) {
this.balance = initialBalance;
}

deposit(amount: number): void {
this.balance += amount; // ✅ 类内部可访问
}

getBalance(): number {
return this.balance;
}
}

const account = new BankAccount(1000);
// console.log(account.balance); // ❌ 编译错误:属性"balance"为私有属性
console.log(account.getBalance()); // ✅ 通过公开方法访问

TypeScript 的 private 只在编译时有效。编译为 JavaScript 后,balance 仍然是普通属性,运行时仍可通过 account['balance'] 访问。如果需要真正的运行时硬隐私,应使用 ES2022 的 # 私有字段。

1
2
3
4
5
6
7
8
9
10
class SecureAccount {
#balance: number; // ES2022 硬私有字段

constructor(initial: number) {
this.#balance = initial;
}
}

const sec = new SecureAccount(1000);
// sec.#balance; // ❌ 语法错误:私有字段在运行时也被保护

选择原则

  • 库/应用内部使用private(或 protected)足够,编译时检查已能防止大部分误用。
  • 需要绝对隐私(如安全数据):使用 # 私有字段。
  • 类型层面完全隔离:TypeScript 的 private 还会影响类型兼容性——拥有 private 成员的两个不同类型不会被视为兼容,即使结构相同。

2.3 protected:类内部与子类可访问

protected 成员在 private 的基础上,还允许派生类(子类)访问。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Base {
protected secret: string;

constructor(secret: string) {
this.secret = secret;
}
}

class Derived extends Base {
reveal(): string {
return this.secret; // ✅ 子类可访问 protected 成员
}
}

const d = new Derived('classified');
console.log(d.reveal()); // 'classified'
// console.log(d.secret); // ❌ 编译错误:外部不可访问

2.4 构造函数参数属性(Parameter Properties)

TypeScript 提供了一种简写语法:在构造函数参数前添加访问修饰符,可以同时声明属性、标注类型、并赋值

1
2
3
4
5
6
7
8
9
10
11
12
class User {
constructor(
public name: string,
private age: number,
protected email: string
) {}
}

const u = new User('Alice', 30, 'alice@example.com');
console.log(u.name); // ✅ public
// console.log(u.age); // ❌ private
// console.log(u.email); // ❌ protected

这等价于:

1
2
3
4
5
6
7
8
9
10
11
class User {
public name: string;
private age: number;
protected email: string;

constructor(name: string, age: number, email: string) {
this.name = name;
this.age = age;
this.email = email;
}
}

构造函数参数属性是 TypeScript 中最常用的特性之一,极大减少了类的样板代码。


3. 抽象类(Abstract Classes)

使用 abstract 关键字声明的类不能直接实例化——它只能作为基类被继承。抽象类中可以包含抽象方法(无实现体,要求子类必须实现)和具体方法(有实现体,可被子类继承或重写)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
abstract class Shape {
abstract getArea(): number; // 抽象方法——子类必须实现

describe(): string {
return `This shape has area: ${this.getArea()}`; // 具体方法可调用抽象方法
}
}

class Circle extends Shape {
constructor(private radius: number) {
super();
}

getArea(): number {
return Math.PI * this.radius ** 2;
}
}

class Rectangle extends Shape {
constructor(private width: number, private height: number) {
super();
}

getArea(): number {
return this.width * this.height;
}
}

// const shape = new Shape(); // ❌ 编译错误:无法创建抽象类的实例
const circle = new Circle(5);
console.log(circle.describe()); // 'This shape has area: 78.539...'

const rect = new Rectangle(4, 6);
console.log(rect.describe()); // 'This shape has area: 24'

抽象类的关键规则

  • 抽象方法必须在抽象类中声明。
  • 子类必须实现所有抽象方法,否则自身也必须声明为抽象类。
  • 抽象类可以有构造函数(通过 super() 调用),可以有非抽象的成员。
  • 抽象类可以声明 abstract 属性访问器(getter/setter)。

4. implements:确保类满足接口契约

implements 关键字用于检查一个类是否实现了某个接口的所有成员。一个类可以实现多个接口(多重实现)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
interface Flyable {
fly(): void;
}

interface Swimable {
swim(): void;
}

class Duck implements Flyable, Swimable {
fly(): void {
console.log('Duck is flying.');
}

swim(): void {
console.log('Duck is swimming.');
}
}

implementsextends 的区别

  • extends:继承实现(父类的方法和属性被子类获得)。
  • implements:仅继承契约(类必须提供接口中声明的所有成员,但不继承任何实际代码)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
interface Printable {
print(): void;
}

// ✅ 类可以实现接口
class Report implements Printable {
print() { console.log('Printing report...'); }
}

// ✅ 多个实现
class Photo implements Printable, Flyable {
print() { console.log('Printing photo...'); }
fly() { console.log('Photo cannot fly, but must implement it'); }
}

使用场景implements 常用于确保多个类共享相同的公共 API——例如不同的支付方式(WechatPayAlipay)都实现 PaymentMethod 接口,从而可以在策略模式中互相替换。


5. 最佳实践与常见陷阱

5.1 优先使用 private,按需开放

默认将成员设为 private,仅在需要外部访问时才提升为 publicprotected。这遵循“最小权限原则”,减少类之间的耦合。

5.2 private vs # 私有字段

特性 private 修饰符 # 私有字段 (ES2022)
生效时机 仅编译时检查 编译时 + 运行时硬隐私
能否被子类访问
运行时通过 obj[name] 访问 ✅ 可以(失去类型保护) ❌ 完全不可访问
类型兼容性影响 影响(private 成员不同 → 类型不兼容) 影响(# 字段不同 → 类型不兼容)
推荐场景 库的内部实现、应用层逻辑保护 高安全性数据、必须运行时隔离的场景

5.3 抽象类 vs 接口

对比维度 抽象类 接口
实例化 不能直接实例化 不能实例化
包含实现 可以包含具体方法和属性 仅声明,不能包含实现
继承 单继承(只能 extends 一个抽象类) 多实现(可以 implements 多个接口)
适用场景 共享部分实现,提供默认行为,约束子类必须实现某些方法 仅定义契约,不提供任何代码

课后练习

一、概念自测(选择题 / 填空题)

  1. (单选) TypeScript 中,一个类实现多个接口应使用哪个关键字?
    A. extends
    B. implements
    C. inherit
    D. include

  2. (单选) 关于抽象类,以下描述正确的是?
    A. 抽象类可以直接实例化。
    B. 抽象类中只能包含抽象方法。
    C. 子类继承抽象类后,必须实现所有抽象方法,否则自身也必须声明为抽象类。
    D. 抽象类不能有构造函数。

  3. (填空) TypeScript 中,可以通过在构造函数参数前添加 ______ 修饰符来同时声明并初始化类属性(语法糖)。

  4. (多选) 以下哪些是 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,通过构造函数接收 widthheight(使用 private 修饰符),实现 getArea
  • 定义 Square 类,继承 Rectangle,其构造函数只接收一个 side 参数,调用 super(side, side)
  • 定义一个接口 Drawable,包含 draw(): void 方法。
  • Circle 实现 Drawable,在 draw 中输出字符串描述。
  • 添加 JSDoc 注释说明每个类和接口的用途。

任务要求:请写出一段完整的中文提示词,发送给 AI,使其生成符合上述要求的 TypeScript 代码。提示词中需明确指定抽象类的定义、继承关系、接口实现和访问修饰符的使用。

三、Agent 模式下的提示词示例

你是一个资深前端开发 Agent。请创建一个 TypeScript 文件 src/shapes.ts,定义几何形状的类层次结构。要求:

  1. abstract class Shape:包含 abstract getArea(): number 和具体方法 describe(): string,返回 "该图形面积为 " + this.getArea().toFixed(2)
  2. class Circle extends Shape:构造函数接收 private radius: number,实现 getArea() 返回 Math.PI * radius ** 2
  3. class Rectangle extends Shape:构造函数接收 private width: number, private height: number,实现 getArea() 返回 width * height
  4. class Square extends Rectangle:构造函数接收 private side: number,调用 super(side, side)
  5. interface Drawable { draw(): void; },让 Circle 实现 Drawabledraw() 输出 "绘制一个半径为 X 的圆形"
  6. 为每个类和方法添加 JSDoc 注释。在末尾添加示例代码:创建 Circle(5)Rectangle(4, 6)Square(3),分别调用 describe() 并打印,对 Circle 调用 draw()
  7. 使用 ESM 导出所有类。完成后输出完整文件内容。

四、面试真题与参考答案

题目(字节跳动前端面试题):

请详细比较 TypeScript 中 interfaceabstract class 的异同。在什么场景下应优先选择抽象类,什么场景下优先选择接口?

参考答案

对比维度 interface abstract class
实例化 不能实例化 不能实例化
包含实现 不能包含任何方法实现(仅声明签名) 可以包含具体方法的实现和属性
访问修饰符 成员默认为 public,不支持 private/protected 支持 publicprivateprotected
继承/实现 一个类可以实现多个接口 一个类只能继承一个抽象类(单继承)
编译产物 不生成任何 JavaScript 代码(仅用于类型检查) 生成实际的 JavaScript 类代码
适用场景 定义纯粹的类型契约,不提供任何实现 多个子类需要共享部分实现,同时强制子类实现特定方法

选择原则

  • **优先使用 interface**:当仅需要描述对象的形状或定义类型契约时。interface 更轻量、更灵活,且支持声明合并。
  • 使用 abstract class:当一组相关的类需要共享代码(如工具方法、默认行为),同时又要强制子类提供特定方法的实现时。例如,框架中的 BaseComponent 提供通用的生命周期管理,但强制子类实现 render 方法。
  • 两者结合:在大型项目中,通常用 interface 定义公共 API 契约,用 abstract class 提供部分默认实现,实现类同时 extends 抽象类和 implements 接口。

课后练习答案

一、概念自测答案

  1. B

    • 解析:implements 用于类实现接口。extends 用于类继承类或接口继承接口。
  2. C

    • 解析:抽象类不能实例化(A 错误),可以包含具体方法(B 错误),可以有构造函数(D 错误)。子类必须实现所有抽象方法,否则自身也必须为抽象类。
  3. public(或 privateprotectedreadonly

    • 解析:构造函数参数属性语法(Parameter Properties)通过在参数前添加访问修饰符实现,例如 constructor(public name: string) {}
  4. A、B、C

    • 解析:TypeScript 支持 publicprivateprotected 三种访问修饰符。internal 是 C# 的修饰符,TypeScript 中没有。

二、AI 编程任务参考答案(提示词示例)

示例提示词
“请生成一个 TypeScript 文件,定义几何形状的类层次结构。要求:

  • 抽象类 Shape,包含 abstract getArea(): numberdescribe(): 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(): voidCircle 实现该接口。
  • 添加 JSDoc 注释和示例代码。直接输出完整代码。”
CATALOG
  1. 1. 第68课:类与访问修饰符——public/private/protected、抽象类、implements
    1. 1.1. 1. 类的基本类型注解
    2. 1.2. 2. 访问修饰符:public、private、protected
      1. 1.2.1. 2.1 public(默认)
      2. 1.2.2. 2.2 private:仅类内部可访问
      3. 1.2.3. 2.3 protected:类内部与子类可访问
      4. 1.2.4. 2.4 构造函数参数属性(Parameter Properties)
    3. 1.3. 3. 抽象类(Abstract Classes)
    4. 1.4. 4. implements:确保类满足接口契约
    5. 1.5. 5. 最佳实践与常见陷阱
      1. 1.5.1. 5.1 优先使用 private,按需开放
      2. 1.5.2. 5.2 private vs # 私有字段
      3. 1.5.3. 5.3 抽象类 vs 接口
    6. 1.6. 课后练习
      1. 1.6.1. 一、概念自测(选择题 / 填空题)
      2. 1.6.2. 二、AI 编程任务:编写面向 AI 的提示词
      3. 1.6.3. 三、Agent 模式下的提示词示例
      4. 1.6.4. 四、面试真题与参考答案
    7. 1.7. 课后练习答案
      1. 1.7.1. 一、概念自测答案
      2. 1.7.2. 二、AI 编程任务参考答案(提示词示例)