类、继承、抽象、访问控制与组合

如果你学过 Java,看到 class、extends 和 private 会觉得很熟悉,但先别把原来的规则原样搬过来。这里从一个类的实例和方法开始,分清哪些约束只在类型检查时存在,哪些会真正留在 JavaScript 运行时。

运行时真正存在的是 JavaScript 的类与原型关系,TypeScript 在上面增加检查规则。看到一个类成员时,可以试着问:这行代码编译后还在吗?用这个问题区分类行为与类型约束,会比直接套用 Java 或 C++ 的规则稳妥。

1. 字段、构造器与参数属性

TypeScript
class Account { static readonly kind = "account" readonly createdAt = new Date() constructor( public readonly id: string, private balanceInCents: number, ) { if (!Number.isSafeInteger(balanceInCents) || balanceInCents < 0) { throw new RangeError("initial balance must be a nonnegative safe integer") } } deposit(amountInCents: number): void { if (!Number.isSafeInteger(amountInCents) || amountInCents <= 0) { throw new RangeError("amount must be a positive integer") } const next = this.balanceInCents + amountInCents if (!Number.isSafeInteger(next)) throw new RangeError("balance overflow") this.balanceInCents = next } get balance(): number { return this.balanceInCents / 100 } }

参数属性是 TypeScript 简写,会生成实例字段赋值。private 是 TypeScript 可见性约束;JavaScript #balance 才是由运行时强制的私有字段。

2. TypeScript private 与 JavaScript #private

TypeScript
class Vault { #token: string constructor(token: string) { this.#token = token } matches(candidate: string): boolean { return this.#token === candidate } }
特性 private token #token
类型检查阶段限制 是 是
运行时真正不可从类外访问 否,普通属性仍存在 是,语言级私有槽
目标较旧 JS 时 普通属性 编译器可能用辅助结构降级

3. 继承与覆写

TypeScript
abstract class Formatter { abstract format(value: unknown): string print(value: unknown): void { console.log(this.format(value)) } } class JsonFormatter extends Formatter { override format(value: unknown): string { return JSON.stringify(value, null, 2) ?? "(无 JSON 文本)" } }

建议开启 noImplicitOverride,让重写意图显式。抽象类在运行时仍是类,但 abstract 限制主要由 TypeScript 检查。

JSON.stringify 并不对所有值返回字符串,示例补上 undefined 的显示回退;循环引用或 BigInt 等仍可能抛错,需要调用方处理。Account 的整数余额也必须验证安全范围,仅 Number.isInteger 不能保证大整数仍然精确。余额除以 100 是显示用的 number,不是通用十进制定点金额方案。

4. 静态侧与实例侧

TypeScript
interface ClockInstance { now(): Date } interface ClockConstructor { new (): ClockInstance readonly timezone: string } function installClock(Clock: ClockConstructor): ClockInstance { console.log(Clock.timezone) return new Clock() }

implements 检查实例侧,不自动检查构造器与静态成员。需要静态契约时单独描述构造签名。

5. 初始化顺序陷阱

派生类初始化大致经历:基类字段、基类构造器、派生类字段、派生类构造器。基类构造期间调用可覆写方法可能访问尚未初始化的派生字段,因此构造器应避免调用多态方法。

这是字段与构造工作的简化顺序,不表示派生构造器的整个函数最后才开始执行:派生构造器可以在 super 前执行不访问未初始化 this 的代码;super 返回后,派生实例字段按规则初始化,再继续后续构造代码。

TypeScript
class Base { constructor() { // 避免在这里调用 this.render() } }

6. 组合优先于深继承

TypeScript
interface Logger { log(message: string): void } class UserService { constructor(private readonly logger: Logger) {} create(name: string): void { this.logger.log(`create:${name}`) } }

组合让依赖契约更小、测试替身更容易,避免脆弱基类和继承层级耦合。继承适用于明确“is-a”关系与稳定替换原则,不应用于简单复用几行实现。

7. this 类型与流式 API

TypeScript
class QueryBuilder { protected clauses: string[] = [] where(expression: string): this { this.clauses.push(`WHERE ${expression}`) return this } } class UserQuery extends QueryBuilder { active(): this { return this.where("active = true") } }

多态 this 保留派生类链式调用类型。

8. 与 Java/C++ 的关键差异

  • JavaScript/TypeScript 使用原型链,类语法不改变这一底层事实。
  • TypeScript 默认结构兼容;Java/C++ 类类型通常以声明身份和继承关系为核心。
  • TypeScript 没有 C++ 的析构函数、确定性 RAII、对象值语义和多继承实现模型。
  • TypeScript protected/private 多为静态限制;#private 是 JavaScript 运行时机制。

参考:Classes、MDN Classes。