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 可见性的关键差异:

修饰符JavaTypeScript
默认(不写)包级私有public
private类外完全不可见(反射除外)仅编译期检查,编译成 JS 后运行时可访问
protected子类+同包子类,无包概念
无 default/friend--

特别注意第二行:TS 的 private编译期约定,产出的 JS 里该字段就是普通属性。若需要语言级硬私有,用 ES 的 #fieldES6+ 特性 第 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 差异总结表

维度JavaTypeScript
默认可见性包私有public
private 强度运行时(字节码层面)编译期检查,产物中裸露
接口判定名义化:须显式 implements结构兼容即可,implements 仅文档化
多重继承不支持(接口多实现)同样不支持,extends 单继承
抽象类支持支持,语义一致
静态成员staticstatic
泛型协变逆变通配符 ? 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。