3.2 类与继承


3.2 类与继承

这节在地图里接在接口之后:接口只描述形状,类同时带形状和行为。TS 在 ES 类语法上叠加了访问修饰符、参数属性、抽象类等类型能力。我们讲清哪些是运行时的、哪些只在编译期存在,避免"写了 private 却能被 JS 绕过"的误会。

类既是值也是类型

ES6 的 class 在运行时是构造函数;TS 额外把它当类型用。这是"类同时是值与类型"的双面性。

class Point { x: number; y: number; constructor(x: number, y: number) { this.x = x; this.y = y; } move(dx: number) { this.x += dx; } } const p: Point = new Point(1, 2); // 左边的 Point 是类型,右边的是值

输入输出:编译后类照常是构造函数,但 p: Point 的检查在编译期发生。类型层面的 Point 只包含 x/y/move 的形状。

修饰符:编译期契约,不是运行时锁

public/private/protected 只在编译期约束谁能动这个成员,编译后和普通属性无异。

class Vault { public name: string; private secret: string; // 仅类内可见(编译期) constructor(name: string, secret: string) { this.name = name; this.secret = secret; } } const v = new Vault("保险箱", "123"); v.name; // OK // v.secret; // 报错:属性 private,外部不可访问

背景:想约束"secret 不能被外部改"。操作:标 private。结果:编译期外部访问被拦。解读:这只是给编译器看的契约,运行时 v.secret 依然能拿到——别用它当安全边界。变式:用 #secret(ES 私有字段)才是运行时真私有。

参数属性:一行顶三行

构造函数参数前加修饰符,自动生成并赋值字段,省掉样板。

class User { constructor( public id: number, public name: string, private token: string ) {} } const u = new User(1, "张三", "tk"); u.id; // OK // u.token 报错,private

输入输出:等价于手写 id; name; token 三字段加赋值。少写代码的同时契约不变。

继承与 implements

类用 extends 继承,implements 声明实现一个接口——后者只检查形状,不继承实现。

interface Loggable { log(): void; } class FileLogger implements Loggable { log() { console.log("写文件"); } // 必须实现 log,否则报错 }

implements 的价值:它把"这个类承诺满足某契约"钉死,重构时若删了 log 方法,编译立刻失败,而不是等到调用处才崩。

抽象类:留接口、押实现

abstract class Shape { abstract area(): number; // 子类必须实现 describe(): string { return `面积 ${this.area()}`; } } class Circle extends Shape { constructor(private r: number) { super(); } area(): number { return Math.PI * this.r ** 2; } }

背景:多个形状都要 area(),但算法各异。操作:抽象基类定契约、给公共方法。结果:子类强制实现 area,公共 describe 复用。解读:抽象类既管"必须有什么"(abstract)又管"共享什么"(具体方法),是接口与具体类之间的中间层。

下图对比接口、抽象类、具体类的职责边界:

03-02-fig01-2

存取器 get/set 的类型契约

TS 给类的存取器做类型检查:读返回类型、写接收类型,可不一致(常见只读场景只写 get)。

class Temperature { private _c = 0; get celsius(): number { return this._c; } set celsius(v: number) { if (v < -273.15) throw new Error("低于绝对零度"); this._c = v; } get fahrenheit(): number { return this._c * 9 / 5 + 32; } } const t = new Temperature(); t.celsius = 25; // OK console.log(t.fahrenheit); // 77 // t.fahrenheit = 0; // 报错:只有 get 没有 set,只读

背景:温度转换、单位归一常需"读一个、写另一个"。操作:用 get 暴露派生值、用 set 做写入校验。结果:契约把"非法赋值"挡在入口。解读:存取器把"计算属性"纳入类型系统,调用方像用普通字段一样用,类型与运行时都正确。

静态成员与 this 类型

静态成员属于类本身而非实例,常放工厂方法或全局计数;TS 还提供 this 类型让链式调用返回精确子类型。

class Builder { private parts: string[] = []; add(p: string): this { // this 类型:返回当前子类的精确类型 this.parts.push(p); return this; } build(): string[] { return this.parts; } } class TagBuilder extends Builder { tag(name: string): this { console.log(name); return this; } } const r = new TagBuilder().add("a").tag("x").add("b").build(); // 全程类型保留

背景:流式 API(如查询构造器)要"每一步都能继续调,且类型不丢"。操作:方法返回 this。结果:继承后链式调用仍保留子类型信息。解读:this 类型是类里少见的"多态返回",比写死基类更精准。

抽象类实战:模板方法

抽象类配具体方法,能实现"模板方法"模式——骨架固定、步骤由子类填。

abstract class Report { abstract fetch(): string[]; abstract render(rows: string[]): void; run() { // 模板方法:顺序固定 const rows = this.fetch(); this.render(rows); } } class CsvReport extends Report { fetch() { return ["a,b", "1,2"]; } render(rows: string[]) { rows.forEach((r) => console.log(r)); } } new CsvReport().run();

背景:多个报表步骤一致、数据源与渲染各异。操作:抽象类定 run 骨架,子类填 fetch/render。结果:复用流程、隔离变化。解读:这是接口给不了的能力(接口无实现),正好落在抽象类中段位置。

工程取舍:组合优于继承

TS 不阻止你写深继承树,但经验上超过两层的继承往往不如组合好维护。一个类需要多种能力时,用 implements 多个接口 + 把能力拆成独立模块,比堆父类更灵活,也不容易撞上"菱形继承"。

泛型类与混入:复用行为而不堆继承

当多个类需要同一份能力(例如可序列化),比起深继承,用泛型基类或混入函数更灵活,也不会撞上菱形继承。

// 可序列化基类:把"能转 JSON"抽成通用能力 abstract class Serializable<T> { abstract toJSON(): T; clone(): T { return JSON.parse(JSON.stringify(this.toJSON())); } } class Account extends Serializable<{ id: number; name: string }> { constructor(public id: number, public name: string) { super(); } toJSON() { return { id: this.id, name: this.name }; } } const a = new Account(1, "主账户"); const b = a.clone(); // b: { id:number; name:string },无需重写克隆逻辑

输入输出:a.clone() 得到的 b 是纯数据对象而非 Account 实例——这正是"序列化能力"与"类身份"分离的典型取舍,多数场景够用。若必须保留实例类型,就要走结构化克隆或混入而非继承。

实战:用接口约束构造签名

有时你关心的不是"它是哪个类",而是"它能用 new 造出某形状"。构造签名把这点写进契约。

interface PointCtor { new (x: number, y: number): { x: number; y: number }; } function make<T>(Ctor: PointCtor, x: number, y: number): T { return new Ctor(x, y) as unknown as T; } class Pt { constructor(public x: number, public y: number) {} } const p = make<Pt>(Pt, 3, 4); // p: Pt,构造契约被类型守住

背景:工厂或依赖容器常需要"传入某个类,返回其实例"。操作:用 new 签名描述构造行为。结果:传入不满足构造签名的类会编译报错。解读:构造签名让你在类型层约束"可实例化性",比 any 兜底安全,也比硬编码具体类灵活。

本节要点回顾

  • 类同时是值与类型
  • private/protected 是编译期契约,不是运行时安全
  • 参数属性减少样板,implements 钉死契约,abstract 押实现
  • 组合常优于深继承

⚠️ 不要把 private 当权限控制。编译后它就是普通属性,运行时任何代码都能访问;真正要运行时私有用 # 字段。

💡 给类加 implements SomeInterface,能迫使重构时不忘实现方法,比纯注释提醒可靠,也顺带把契约显式化。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U