03 - TS 与面向对象
前置:TS 进阶类型。TS 的 class 在 JS 之上补齐了 Java 程序员熟悉的大部分面向对象设施:可见性修饰符、抽象类、接口实现。本章对照 继承与多态 与 接口与抽象类 展开。
1. class 完整修饰符
1.1 四个成员修饰符
class Account {
public id: number; // 公开:任何地方可访问(默认值,可省略)
private balance: number; // 私有:仅类内部可访问
protected owner: string; // 受保护:类内部 + 子类
readonly createdAt: Date; // 只读:初始化后不可再赋值
constructor(id: number, owner: string) {
this.id = id;
this.balance = 0;
this.owner = owner;
this.createdAt = new Date();
}
deposit(amount: number): void {
if (amount <= 0) throw new Error("金额必须为正");
this.balance += amount; // 内部可写 private 字段
}
getBalance(): number {
return this.balance;
}
}
const acc = new Account(1, "张三");
acc.deposit(100);
console.log(acc.getBalance()); // 100
// acc.balance; // 编译错误:balance 是私有的与 Java 可见性的关键差异:
| 修饰符 | Java | TypeScript |
|---|---|---|
| 默认(不写) | 包级私有 | public |
| private | 类外完全不可见(反射除外) | 仅编译期检查,编译成 JS 后运行时可访问 |
| protected | 子类+同包 | 子类,无包概念 |
| 无 default/friend | - | - |
特别注意第二行:TS 的 private 是编译期约定,产出的 JS 里该字段就是普通属性。若需要语言级硬私有,用 ES 的 #field(ES6+ 特性 第 6 节),两者可共存——#field 运行时强制,private 表达设计意图。
1.2 参数属性:构造器的语法糖
class User {
constructor(
public name: string, // 自动声明并赋值同名成员字段
private password: string,
readonly id: number,
) {} // 构造器体可以为空
}
// 等价于手写三个字段声明 + 三句 this.xxx = xxx 赋值这是 TS 独有的简写,Java 没有。DTO/实体类场景能省掉大量样板代码。
1.3 getter / setter 与静态成员
class Temperature {
private _celsius = 0;
get celsius(): number { // 取值器
return this._celsius;
}
set celsius(value: number) { // 存值器:可以加校验逻辑
if (value < -273.15) throw new Error("低于绝对零度");
this._celsius = value;
}
static freezingPoint = 0; // 静态字段:属于类
static fromFahrenheit(f: number): Temperature { // 静态工厂方法
return new Temperature((f - 32) / 1.8);
}
}
const t = Temperature.fromFahrenheit(100);
t.celsius = 40; // 触发 setter 校验
console.log(Temperature.freezingPoint);用法与 Java getter/setter 一致,区别是调用处像属性一样直接赋值(t.celsius = 40),不需要 setCelsius()。
2. abstract 抽象类与 implements
2.1 抽象类
abstract class Shape {
protected name: string;
constructor(name: string) { this.name = name; }
abstract area(): number; // 抽象方法:子类必须实现
abstract perimeter(): number;
describe(): string { // 具体方法:子类直接继承复用
const a = this.area(); // 可以调用抽象方法
return `${this.name},面积 ${a.toFixed(2)}`;
}
}
class Circle extends Shape {
constructor(private r: number) { super("圆"); }
area(): number { return Math.PI * this.r ** 2; }
perimeter(): number { return 2 * Math.PI * this.r; }
}
class Rect extends Shape {
constructor(private w: number, private h: number) { super("矩形"); }
area(): number { return this.w * this.h; }
perimeter(): number { return 2 * (this.w + this.h); }
}
const shapes: Shape[] = [new Circle(2), new Rect(3, 4)];
shapes.forEach(s => console.log(s.describe()));与 Java 抽象类几乎一一对应:不能实例化、抽象方法强制子类实现、可含具体实现。多态的调度机制也相同——shapes.forEach 时运行期决定调谁的 area()。
2.2 实现接口 vs 结构兼容
interface Serializable {
toJSON(): string;
}
class Order implements Serializable { // 显式声明"我实现了这个接口"
constructor(private items: string[]) {}
toJSON(): string { return JSON.stringify(this.items); }
}微妙之处在于:由于结构化类型,即使不写 implements,只要 Order 形状满足 Serializable,它照样可以被当作 Serializable 使用:
function persist(data: Serializable) { /* ... */ }
persist(new Order(["x"])); // 合法,无论有没有 implements 声明那 implements 还有什么用?两点:一是意图文档化(读者立刻知道这个类承诺了什么契约);二是让编译器检查”承诺是否兑现”,防止后续重构悄悄弄丢了某方法导致所有调用点报错。Java 里 implements 是身份认证,TS 里更多是自我约束声明。
再看一个体现差异的实战片段——测试替身(test double)几乎免费:
// 生产代码只依赖形状
interface Mailer { send(to: string, body: string): Promise<void> }
// 测试里不需要框架 mock,直接给一个同形状对象
const spyMailer: Mailer = {
send: async (to) => void sent.push(to),
};在 Java 中同样的替身通常要引入 Mockito 之类的动态代理库,因为名义类型要求”是某类的实例”;TS 只看形状,普通字面量即可顶替。
3. 泛型类
容器类是泛型的主场,写法与 Java 高度相似:
class Stack<T> {
private items: T[] = [];
push(item: T): void { this.items.push(item); }
pop(): T | undefined { return this.items.pop(); }
peek(): T | undefined { return this.items[this.items.length - 1]; }
get size(): number { return this.items.length; }
}
const numStack = new Stack<number>();
numStack.push(1);
numStack.push(2);
numStack.pop()?.toFixed(2); // 类型精确为 number | undefined
const strStack = new Stack<string>(); // 每个实例独立锁定 T一个更实用的例子——带缓存的泛型仓库层:
interface Entity { id: number }
class Repository<T extends Entity> {
private store = new Map<number, T>();
save(entity: T): void { this.store.set(entity.id, entity); }
findById(id: number): T | undefined { return this.store.get(id); }
findAll(): T[] { return [...this.store.values()]; }
delete(id: number): boolean { return this.store.delete(id); }
}
interface Task extends Entity { title: string; done: boolean }
const taskRepo = new Repository<Task>();
taskRepo.save({ id: 1, title: "学习泛型", done: false });
taskRepo.findById(1)?.title; // 精确推出 Task | undefined
// taskRepo.save({ title: "缺 id" }); // 编译错误,约束生效T extends Entity 就是泛型约束在类上的应用:只接受有 id 主键的类型,仓库逻辑得以通用化——这正是后端 ORM 前端版(如 TypeORM 的 Repository)的设计原型。
4. 装饰器概念
装饰器(Decorator)是附加在类/方法/属性上的特殊语法,本质是”接收目标并加工它的函数”:
function log(target: any, methodName: string, desc: PropertyDescriptor) {
const original = desc.value;
desc.value = function (...args: unknown[]) {
console.log(`调用 ${methodName},参数`, args);
return original.apply(this, args);
};
}
class Calculator {
@log // 不改方法体,注入日志行为
add(a: number, b: number): number {
return a + b;
}
}
new Calculator().add(1, 2); // 控制台先打印 "调用 add,参数 [1, 2]"需要知道的三件事:
- Angular、NestJS 大量使用装饰器组织代码(
@Component、@Controller等),读这类框架源码前必须认识它 - 它长期处于 TC39 提案阶段(实验性特性),使用需开启
experimentalDecorators配置;标准版已于近年落地,两版语法有差异 - 功能上类似 Java 注解 + AOP:Java 的注解本身只是标记,需框架反射读取;装饰器则直接在定义时改写目标,机制更直接
学习阶段理解概念即可,不必手写生产级装饰器。
5. 实用工具模式
5.1 配置对象模式
参数超过三个时收敛为配置对象,配合必选/可选与默认值:
interface HttpClientOptions {
baseURL: string;
timeout?: number; // 可选
retries?: number;
headers?: Record<string, string>;
}
class HttpClient {
private opts: Required<Pick<HttpClientOptions, "timeout" | "retries">>
& Pick<HttpClientOptions, "baseURL" | "headers">;
constructor(options: HttpClientOptions) {
// 解构 + 默认值合并,调用方只传关心的项
this.opts = {
timeout: 5000,
retries: 3,
...options,
} as typeof this.opts;
}
}
new HttpClient({ baseURL: "https://api.example.com" }); // 其余走默认5.2 建造者模式的简化
Java 中 Builder 用于规避多参构造器的可读性灾难。TS 下有两种轻量替代:
// 方案一:全可选配置 + 必填项拆出(多数场景的最优解)
class Server {
private constructor(
public host: string,
public port: number,
public tls: boolean,
public maxConns: number,
) {}
static create(host: string, port = 8080): ServerBuilder {
return new ServerBuilder(host, port);
}
start(): void { console.log(`${this.host}:${this.port} tls=${this.tls}`); }
}
// 真正需要分步构建时,用返回 this 的链式结构
class ServerBuilder {
private tls = false;
private maxConns = 100;
constructor(private host: string, private port: number) {}
enableTls(): this { // 返回 this 实现链式调用
this.tls = true;
return this;
}
setMaxConns(n: number): this {
this.maxConns = n;
return this;
}
build(): Server {
return new Server(this.host, this.port, this.tls, this.maxConns);
}
}
Server.create("localhost", 3000)
.enableTls()
.setMaxConns(500)
.build()
.start();对比 Java Builder 的收益:不需要 Lombok @Builder 或手写内部静态类也能获得同样体验;而”必填参数走函数签名、可选配置走链式”这条原则适用于一切 TS 构造场景。
5.3 策略模式 TS 写法
用对象字面量替代传统策略类的继承体系(组合优于继承的极致体现):
type SortStrategy = (a: number, b: number) => number;
const strategies: Record<"asc" | "desc" | "random", SortStrategy> = {
asc: (a, b) => a - b,
desc: (a, b) => b - a,
random: () => Math.random() - 0.5,
};
function sortNums(arr: number[], mode: keyof typeof strategies): number[] {
return [...arr].sort(strategies[mode]);
}
sortNums([3, 1, 2], "asc"); // [1, 2, 3]
sortNums([3, 1, 2], "desc"); // [3, 2, 1]对比 Java 版策略模式:无需 Context 接口、无需每个策略一个类,函数即策略,Record 保证三种模式全部注册齐全。这是 TS 中最常见的设计模式落地形态——很多 GoF 模式在函数式风格下大幅瘦身,参考 设计模式 对照思考哪些模式被语言特性吸收了。
6. 与 Java OOP 差异总结表
| 维度 | Java | TypeScript |
|---|---|---|
| 默认可见性 | 包私有 | public |
| private 强度 | 运行时(字节码层面) | 编译期检查,产物中裸露 |
| 接口判定 | 名义化:须显式 implements | 结构兼容即可,implements 仅文档化 |
| 多重继承 | 不支持(接口多实现) | 同样不支持,extends 单继承 |
| 抽象类 | 支持 | 支持,语义一致 |
| 静态成员 | static | static |
| 泛型协变逆变 | 通配符 ? extends/super | 只做不变处理,结构化兜底 |
| 注解/AOP | 反射 + 框架 | 装饰器直接改写 |
| 构造器重载 | 支持多份 | 不支持,用可选参数模拟 |
总体心法:把 Java 的 OOP 设计直觉迁移过来基本成立,但要时刻记住结构化类型这条底层规则——TS 的”接口”是形状描述而非身份证书。
7. 本章小结
- 修饰符四件套 public/private/protected/readonly;private 是编译期的,#field 才是运行时的。
- 参数属性
constructor(public x: string)是 TS 特有的样板代码消除器。 - 抽象类语义与 Java 一致;结构化类型下 implements 更多是文档和自检。
- 泛型类 + extends 约束是通用仓库层的基石。
- 装饰器 = 定义时改写目标的函数,Angular/NestJS 的核心语法。
- 配置对象与”函数即策略”展示了 TS 下设计模式的轻量化形态。
下一步综合实战:TS 实战:类型安全组件,用 TS 重写 TodoList。