04 - 类

3 阅读3分钟

TypeScript 为 JavaScript 的 class 添加了类型注解访问控制能力。


4.1 基本类定义

class Person {
  name: string;
  age: number;

  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }

  greet(): string {
    return `我是 ${this.name},今年 ${this.age} 岁`;
  }
}

const p = new Person("张三", 25);
console.log(p.greet()); // "我是 张三,今年 25 岁"

简写形式(参数属性)

在构造函数参数前加修饰符,可以自动创建并赋值属性:

class Person {
  // 等同于声明属性 + 在 constructor 中赋值
  constructor(
    public name: string,
    public age: number,
  ) {}

  greet(): string {
    return `我是 ${this.name},今年 ${this.age} 岁`;
  }
}

💡 这是 TS 独有的语法糖,可以大幅减少样板代码。


4.2 访问修饰符

修饰符类内部子类类外部
public
protected
private
class BankAccount {
  public owner: string;         // 谁都能访问(默认)
  protected balance: number;    // 子类可以访问
  private password: string;     // 只有自己能访问

  constructor(owner: string, balance: number, password: string) {
    this.owner = owner;
    this.balance = balance;
    this.password = password;
  }

  public getBalance(): number {
    return this.balance;
  }

  private verify(pwd: string): boolean {
    return pwd === this.password;
  }
}

const account = new BankAccount("张三", 1000, "123456");
account.owner;      // ✅ public
account.balance;    // ❌ protected
account.password;   // ❌ private
account.getBalance(); // ✅ 通过公开方法访问

JS 原生私有字段 #

class Secret {
  #value: string; // JS 原生私有,运行时也无法访问

  constructor(value: string) {
    this.#value = value;
  }

  getValue(): string {
    return this.#value;
  }
}

💡 private 只在编译时检查,# 在运行时也生效。推荐使用 # 来实现真正的私有。


4.3 只读属性

class Config {
  readonly apiUrl: string;

  constructor(url: string) {
    this.apiUrl = url; // ✅ 构造函数中可以赋值
  }

  update() {
    this.apiUrl = "new"; // ❌ 只读,不能修改
  }
}

4.4 继承

class Animal {
  constructor(public name: string) {}

  move(distance: number): void {
    console.log(`${this.name} 移动了 ${distance} 米`);
  }
}

class Dog extends Animal {
  constructor(name: string, public breed: string) {
    super(name); // 必须调用 super()
  }

  bark(): void {
    console.log("汪汪!");
  }

  // 重写父类方法
  move(distance: number): void {
    console.log("🐕 跑起来了!");
    super.move(distance); // 调用父类方法
  }
}

const dog = new Dog("旺财", "柴犬");
dog.bark();    // "汪汪!"
dog.move(10);  // "🐕 跑起来了!" → "旺财 移动了 10 米"

4.5 抽象类

抽象类不能被实例化,只能被继承。它可以包含抽象方法(没有实现的方法):

abstract class Shape {
  abstract area(): number;      // 抽象方法:子类必须实现
  abstract perimeter(): number;

  // 普通方法:子类可以直接使用
  describe(): string {
    return `面积: ${this.area()}, 周长: ${this.perimeter()}`;
  }
}

class Circle extends Shape {
  constructor(private radius: number) {
    super();
  }

  area(): number {
    return Math.PI * this.radius ** 2;
  }

  perimeter(): number {
    return 2 * Math.PI * this.radius;
  }
}

class Rectangle extends Shape {
  constructor(
    private width: number,
    private height: number,
  ) {
    super();
  }

  area(): number {
    return this.width * this.height;
  }

  perimeter(): number {
    return 2 * (this.width + this.height);
  }
}

// const shape = new Shape(); // ❌ 不能实例化抽象类
const circle = new Circle(5);
console.log(circle.describe()); // "面积: 78.54, 周长: 31.42"

4.6 实现接口(implements)

类可以通过 implements 来遵循接口的约定:

interface Printable {
  print(): void;
}

interface Loggable {
  log(message: string): void;
}

// 实现多个接口
class Report implements Printable, Loggable {
  constructor(private title: string) {}

  print(): void {
    console.log(`📄 ${this.title}`);
  }

  log(message: string): void {
    console.log(`[Report] ${message}`);
  }
}

interface vs abstract class

特性interfaceabstract class
多重实现/继承✅ 一个类可实现多个接口❌ 只能继承一个类
有默认实现✅ 可以有普通方法
有构造函数
运行时存在❌ 编译后消失

4.7 静态成员

class MathHelper {
  static PI = 3.14159;

  static square(n: number): number {
    return n * n;
  }

  static cube(n: number): number {
    return n ** 3;
  }
}

// 直接通过类名调用,不需要实例化
MathHelper.PI;           // 3.14159
MathHelper.square(5);    // 25

4.8 Getter 和 Setter

class Temperature {
  private _celsius: number = 0;

  get celsius(): number {
    return this._celsius;
  }

  set celsius(value: number) {
    if (value < -273.15) {
      throw new Error("温度不能低于绝对零度");
    }
    this._celsius = value;
  }

  get fahrenheit(): number {
    return this._celsius * 1.8 + 32;
  }

  set fahrenheit(value: number) {
    this.celsius = (value - 32) / 1.8;
  }
}

const temp = new Temperature();
temp.celsius = 100;
console.log(temp.fahrenheit); // 212
temp.celsius = -300; // ❌ 抛出错误

4.9 类作为类型

在 TypeScript 中,类本身就是一个类型:

class Point {
  constructor(public x: number, public y: number) {}
}

// Point 既是构造函数,也是类型
function distance(a: Point, b: Point): number {
  return Math.sqrt((a.x - b.x) ** 2 + (a.y - b.y) ** 2);
}

// 结构类型:只要形状匹配就行
const p1 = new Point(0, 0);
const p2 = { x: 3, y: 4 }; // 不是 Point 实例,但结构匹配

distance(p1, p2); // ✅ TypeScript 使用结构类型

4.10 单例模式

class Database {
  private static instance: Database;

  private constructor(private connectionString: string) {}

  static getInstance(): Database {
    if (!Database.instance) {
      Database.instance = new Database("mongodb://localhost:27017");
    }
    return Database.instance;
  }

  query(sql: string): void {
    console.log(`执行: ${sql}`);
  }
}

// const db = new Database("..."); // ❌ 构造函数是 private
const db = Database.getInstance(); // ✅ 通过静态方法获取

📝 练习

  1. 创建一个 Stack<T> 类,实现 pushpoppeekisEmpty 方法
  2. 创建抽象类 Logger,有抽象方法 write(msg: string),普通方法 info/warn/error
  3. 实现 ConsoleLoggerFileLogger 继承 Logger
// 参考答案

// 1. 泛型栈
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];
  }

  isEmpty(): boolean {
    return this.items.length === 0;
  }
}

const numStack = new Stack<number>();
numStack.push(1);
numStack.push(2);
numStack.pop(); // 2

// 2 & 3. Logger
abstract class Logger {
  abstract write(msg: string): void;

  info(msg: string): void {
    this.write(`[INFO] ${msg}`);
  }

  warn(msg: string): void {
    this.write(`[WARN] ${msg}`);
  }

  error(msg: string): void {
    this.write(`[ERROR] ${msg}`);
  }
}

class ConsoleLogger extends Logger {
  write(msg: string): void {
    console.log(msg);
  }
}

class FileLogger extends Logger {
  constructor(private filepath: string) {
    super();
  }

  write(msg: string): void {
    // 模拟写文件
    console.log(`写入 ${this.filepath}: ${msg}`);
  }
}