类、继承、抽象、访问控制与组合
如果你学过 Java,看到 class、extends 和 private 会觉得很熟悉,但先别把原来的规则原样搬过来。这里从一个类的实例和方法开始,分清哪些约束只在类型检查时存在,哪些会真正留在 JavaScript 运行时。
运行时真正存在的是 JavaScript 的类与原型关系,TypeScript 在上面增加检查规则。看到一个类成员时,可以试着问:这行代码编译后还在吗?用这个问题区分类行为与类型约束,会比直接套用 Java 或 C++ 的规则稳妥。
1. 字段、构造器与参数属性
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
class Vault {
#token: string
constructor(token: string) {
this.#token = token
}
matches(candidate: string): boolean {
return this.#token === candidate
}
}
| 特性 | private token |
#token |
|---|---|---|
| 类型检查阶段限制 | 是 | 是 |
| 运行时真正不可从类外访问 | 否,普通属性仍存在 | 是,语言级私有槽 |
| 目标较旧 JS 时 | 普通属性 | 编译器可能用辅助结构降级 |
3. 继承与覆写
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. 静态侧与实例侧
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 返回后,派生实例字段按规则初始化,再继续后续构造代码。
class Base {
constructor() {
// 避免在这里调用 this.render()
}
}
6. 组合优先于深继承
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
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。